├── .gitignore ├── README.md ├── common ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── zzzi │ │ └── common │ │ ├── config │ │ ├── DefaultFeignConfiguration.java │ │ └── RedissonConfig.java │ │ ├── constant │ │ ├── RabbitMQKeys.java │ │ ├── RedisDefaultValue.java │ │ └── RedisKeys.java │ │ ├── exception │ │ ├── CommentActionException.java │ │ ├── CommentListException.java │ │ ├── FollowException.java │ │ ├── RelationException.java │ │ ├── UserException.java │ │ ├── UserInfoException.java │ │ ├── VideoException.java │ │ └── VideoListException.java │ │ ├── feign │ │ ├── UserClient.java │ │ └── fallback │ │ │ └── UserClientFallbackFactory.java │ │ ├── result │ │ ├── CommentActionVO.java │ │ ├── CommentListVO.java │ │ ├── CommentVO.java │ │ ├── CommonVO.java │ │ ├── MessageListVO.java │ │ ├── MessageVO.java │ │ ├── UserInfoVO.java │ │ ├── UserRegisterLoginVO.java │ │ ├── UserRelationListVO.java │ │ ├── UserVO.java │ │ ├── VideoFeedListVO.java │ │ ├── VideoListVO.java │ │ └── VideoVO.java │ │ └── utils │ │ ├── COSUtils.java │ │ ├── JwtUtils.java │ │ ├── MD5Utils.java │ │ ├── MinioUploadUtils.java │ │ ├── MinioUtils.java │ │ ├── MultiPartUploadUtils.java │ │ ├── RandomUtils.java │ │ ├── RedisLockUtils.java │ │ ├── SMSUtils.java │ │ ├── SendMessageUtils.java │ │ ├── UpdateTokenUtils.java │ │ ├── UpdateUserInfoUtils.java │ │ ├── UpdateVideoInfoUtils.java │ │ ├── UploadUtils.java │ │ └── VideoUtils.java │ └── resources │ ├── reentrantLock.lua │ ├── reentrantUnLock.lua │ └── unLock.lua ├── gateway ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── zzzi │ │ │ └── gateway │ │ │ ├── GatewayApplication.java │ │ │ ├── config │ │ │ ├── CorsConfig.java │ │ │ └── UnExpectedExitHandler.java │ │ │ └── filter │ │ │ └── GatewayGlobalFilter.java │ └── resources │ │ ├── application.txt │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── zzzi │ └── gateway │ └── GatewayApplicationTests.java ├── pom.xml ├── resource ├── RabbitMQ设计.md ├── app-release.apk ├── img │ ├── RabbitMQ.jpg │ └── framework.jpg └── tiktok.sql ├── user-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── zzzi │ │ │ └── userservice │ │ │ ├── UserServiceApplication.java │ │ │ ├── config │ │ │ ├── MyMetaObjectHandler.java │ │ │ ├── MybatisPlusConfig.java │ │ │ ├── RabbitMQConfig.java │ │ │ ├── ReturnCallBack.java │ │ │ ├── UnExpectedExitHandler.java │ │ │ └── WebConfig.java │ │ │ ├── controller │ │ │ ├── MessageController.java │ │ │ ├── RelationController.java │ │ │ └── UserController.java │ │ │ ├── dto │ │ │ └── UserDTO.java │ │ │ ├── entity │ │ │ ├── MessageDO.java │ │ │ ├── UserDO.java │ │ │ └── UserFollowDO.java │ │ │ ├── interceptor │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── LoginUserInterceptor.java │ │ │ ├── listener │ │ │ ├── FavoriteListenerOne.java │ │ │ ├── FavoriteListenerTwo.java │ │ │ ├── FollowListener.java │ │ │ ├── PostVideoListener.java │ │ │ ├── UnFavoriteListenerOne.java │ │ │ ├── UnFavoriteListenerTwo.java │ │ │ └── UnFollowListener.java │ │ │ ├── mapper │ │ │ ├── MessageMapper.java │ │ │ ├── RelationMapper.java │ │ │ └── UserMapper.java │ │ │ └── service │ │ │ ├── MessageService.java │ │ │ ├── RelationService.java │ │ │ ├── UserService.java │ │ │ └── impl │ │ │ ├── MessageServiceImpl.java │ │ │ ├── RelationServiceImpl.java │ │ │ └── UserServiceImpl.java │ └── resources │ │ ├── application.txt │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── zzzi │ └── userservice │ └── UserServiceApplicationTests.java └── video-service ├── .gitignore ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── zzzi │ │ └── videoservice │ │ ├── VideoServiceApplication.java │ │ ├── config │ │ ├── BinLogEventHandler.java │ │ ├── MyMetaObjectHandler.java │ │ ├── MybatisPlusConfig.java │ │ ├── RabbitMQConfig.java │ │ ├── ReturnCallBack.java │ │ ├── UnExpectedExitHandler.java │ │ └── WebConfig.java │ │ ├── controller │ │ ├── CommentController.java │ │ ├── FavoriteController.java │ │ └── VideoController.java │ │ ├── dto │ │ └── VideoFeedDTO.java │ │ ├── entity │ │ ├── CommentDO.java │ │ ├── FavoriteDO.java │ │ └── VideoDO.java │ │ ├── interceptor │ │ ├── GlobalExceptionHandler.java │ │ └── LoginUserInterceptor.java │ │ ├── listener │ │ ├── CommentListener.java │ │ ├── FavoriteListenerOne.java │ │ ├── FavoriteListenerTwo.java │ │ ├── UnCommentListener.java │ │ ├── UnFavoriteListenerOne.java │ │ └── UnFavoriteListenerTwo.java │ │ ├── mapper │ │ ├── CommentMapper.java │ │ ├── FavoriteMapper.java │ │ └── VideoMapper.java │ │ └── service │ │ ├── CommentService.java │ │ ├── FavoriteService.java │ │ ├── VideoService.java │ │ └── impl │ │ ├── CommentServiceImpl.java │ │ ├── FavoriteServiceImpl.java │ │ └── VideoServiceImpl.java └── resources │ ├── application.txt │ └── banner.txt └── test └── java └── com └── zzzi └── videoservice └── VideoServiceApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | resource/static/ 3 | *.yml 4 | target/ 5 | !.mvn/wrapper/maven-wrapper.jar 6 | !**/src/main/**/target/ 7 | !**/src/test/**/target/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

极简版抖音指南🧭

2 | 3 |

4 | 微服务版、Java实现 5 |

6 | 7 | ### ⛳⛳前言 8 | 9 | 本仓库是字节青训营的官方项目,开发文档参考《[在线接口文档分享](https://apifox.com/apidoc/shared-7b33652d-6080-41bb-a70e-7a165d55daae)》以及字节官方提供的《[极简版抖音App使用说明-青训营版](https://bytedance.larkoffice.com/docs/doccnM9KkBAdyDhg8qaeGlIz7S7)》 10 | 11 | 12 | ### :memo::memo:架构设计 13 | 14 | 系统整体架构图如下图所示: 15 | 16 |

17 | 18 |

19 | 20 | > 需要使用的项目中间件全部使用docker部署到了服务器中 21 | 22 | ### :sun_with_face::sun_with_face:项目特点 23 | 24 | 1. 使用主流的[**微服务**](https://spring.io/projects/spring-cloud)架构进行开发,降低项目的耦合度; 25 | 2. 代码风格参考《[**Java开发手册(黄山版)**](https://github.com/alibaba/p3c/blob/master/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C(%E9%BB%84%E5%B1%B1%E7%89%88).pdf)》进行开发,符合大厂开发规范; 26 | 3. 使用[**Sharding-JDBC**](https://shardingsphere.apache.org/document/4.1.0/cn/manual/sharding-jdbc/)进行数据层的读写分离+分库分表,提升数据库的性能; 27 | 4. 引入[**Seata**](https://seata.apache.org/zh-cn/)解决分布式事务问题,加强程序的健壮性; 28 | 5. 引入安全性更高的[**KDF**](https://mp.weixin.qq.com/s/TcGnktKbZK9hrvNvvO7kgQ)算法加强用户登录注册的验证环节,防止用户密码被暴力破解或者出现彩虹表攻击; 29 | 6. [**RabbitMQ**](https://www.rabbitmq.com/)实现流量削峰,提高系统的可用性; 30 | 7. 使用存储默认值的形式解决[**缓存穿透**](https://xiaolincoding.com/redis/cluster/cache_problem.html#%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F)问题 31 | 8. 缓存过期值随机打散+Sentinel限流缓解[**缓存雪崩**](https://xiaolincoding.com/redis/cluster/cache_problem.html#%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9)问题 32 | 9. **多种**模式的缓存同步方案: 33 | - 同步双写:实时性高的信息同步更新 34 | - 异步通知:RabbitMQ异步更新缓存 35 | - 后台监听MySQL日志,实时更新缓存 36 | 10. 多种模式的`Feed`流,降低推荐视频的延迟: 37 | - 热点用户:投稿作品使用拉模式,不主动更新 38 | - 普通用户:投稿时使用推模式将作品推送到粉丝的收件箱中 39 | 11. 实现**父子模式**的评论,前端显示带层级结构,延迟推送所有的子评论列表 40 | 12. 。。。 41 | 42 | 43 | ### :rabbit::rabbit:RabbitMQ设计 44 | 45 | 项目中涉及到的交换机和队列以及对应的`RoutingKey`如下图所示: 46 | 47 |

48 | 49 |

50 | 51 | > 需要注意的是,项目中专门定义了一个交换机和队列来接收超过重试次数的消息**集中**进行处理 52 | 53 | ### :bug::bug:代码结构 54 | 55 | 整个项目的结构如下: 56 | 57 | ```sh 58 | tiktok:. 59 | ├─common 60 | │ └─src 61 | │ ├─config 62 | │ ├─constant 63 | │ ├─exception 64 | │ ├─feign 65 | │ │ └─fallback 66 | │ ├─result 67 | │ └─utils 68 | ├─gateway 69 | │ └─src 70 | │ │ ├─config 71 | │ │ └─filter 72 | │ └─resources 73 | ├─resource 74 | │ ├─img 75 | │ └─static 76 | │ ├─cover 77 | │ └─video 78 | ├─user-service 79 | │ └─src 80 | │ │ ├─config 81 | │ │ ├─controller 82 | │ │ ├─dto 83 | │ │ ├─entity 84 | │ │ ├─interceptor 85 | │ │ ├─listener 86 | │ │ ├─mapper 87 | │ │ └─service 88 | │ │ └─impl 89 | │ └─resources 90 | └─video-service 91 | └─src 92 | │ ├─config 93 | │ ├─controller 94 | │ ├─dto 95 | │ ├─entity 96 | │ ├─interceptor 97 | │ ├─listener 98 | │ ├─mapper 99 | │ └─service 100 | │ └─impl 101 | └─resources 102 | ``` 103 | 104 | ### :runner::runner:项目运行 105 | 106 | 下载本项目后,需要在各个微服务模块的`application.txt`的后缀名换成`.yml`,之后将每个中间件对应的**地址**替换成自己需要的,然后加上腾讯云cos需要的配置项,例如: 107 | 108 | ```yml 109 | # videoservice中腾讯云上传文件的配置 110 | tencent: 111 | cos: 112 | secretId: yourSecertId 113 | secretKey: yourSecretKey 114 | bucketName: yourBucketName 115 | region: ap-beijing 116 | # userservice中腾讯云发送短信的配置 117 | tencent: 118 | sms: 119 | secretId: yourSecretId 120 | secretKey: yourSecretKey 121 | endpoint: yourEndpoint 122 | region: yourRegion 123 | sdkAppId: yourSdkAppId 124 | signName: yourSignName 125 | templateId: yourTemplateId 126 | signMethod: "HmacSHA256" 127 | ``` 128 | 129 | -------------------------------------------------------------------------------- /common/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/config/DefaultFeignConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.config; 2 | 3 | 4 | import com.zzzi.common.feign.fallback.UserClientFallbackFactory; 5 | import feign.Logger; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | //编写这个配置类是为了注册一些bean 10 | @Configuration 11 | public class DefaultFeignConfiguration { 12 | @Bean 13 | public Logger.Level logLevel(){ 14 | return Logger.Level.BASIC; 15 | } 16 | 17 | @Bean 18 | public UserClientFallbackFactory userClientFallbackFactory(){ 19 | return new UserClientFallbackFactory(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/config/RedissonConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.config; 2 | 3 | import org.redisson.Redisson; 4 | import org.redisson.api.RedissonClient; 5 | import org.redisson.config.Config; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * @author zzzi 11 | * @date 2024/4/13 14:12 12 | * 在这里配置Redisson客户端的地址,不采用起步依赖的方式整合springboot 13 | * 而是手动加入 14 | */ 15 | @Configuration 16 | public class RedissonConfig { 17 | 18 | /** 19 | * @author zzzi 20 | * @date 2024/4/13 14:14 21 | * 如果Redis集群模式下需要使用MultiLock 22 | * 那么就在这里连接多个Redisson客户端即可 23 | * MultiLock眼中所有的Redis节点之间没有主从之分 24 | */ 25 | @Bean 26 | public RedissonClient redissonClient() { 27 | Config config = new Config(); 28 | config.useSingleServer().setAddress("redis://localhost:6379"); 29 | 30 | //创建客户端 31 | return Redisson.create(config); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/constant/RabbitMQKeys.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.constant; 2 | 3 | /** 4 | * @author zzzi 5 | * @date 2024/3/25 18:46 6 | * RabbitMQ的相关设计 7 | */ 8 | public class RabbitMQKeys { 9 | 10 | /** 11 | * MQ中交换机名称 12 | */ 13 | public static final String POST_VIDEO_EXCHANGE = "tiktok.post_video"; 14 | public static final String FOLLOW_EXCHANGE = "tiktok.follow"; 15 | public static final String COMMENT_EXCHANGE = "tiktok.comment"; 16 | public static final String ERROR_EXCHANGE = "error.direct"; 17 | 18 | 19 | /** 20 | * MQ中关于消费失败的key 21 | */ 22 | public static final String ERROR = "error"; 23 | 24 | /** 25 | * MQ中关于点赞的queue 26 | * 点赞和取消点赞需要分开 27 | * 并且点赞针对用户和视频的key也需要分开 28 | */ 29 | public static final String FAVORITE_USER = "work.favorite_user"; 30 | public static final String UN_FAVORITE_USER = "work.un_favorite_user"; 31 | public static final String FAVORITE_VIDEO = "work.favorite_video"; 32 | public static final String UN_FAVORITE_VIDEO = "work.un_favorite_video"; 33 | /** 34 | * MQ中关于评论的key 35 | */ 36 | public static final String COMMENT_KEY = "comment"; 37 | public static final String UN_COMMENT_KEY = "un_comment"; 38 | /** 39 | * MQ中关于关注的key 40 | * 关注和取消关注需要分开 41 | */ 42 | public static final String FOLLOW_KEY = "follow"; 43 | public static final String UN_FOLLOW_KEY = "un_follow"; 44 | 45 | /** 46 | * MQ中关于投稿的key 47 | */ 48 | public static final String VIDEO_POST = "video_post"; 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/constant/RedisDefaultValue.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.constant; 2 | 3 | /** 4 | * @author zzzi 5 | * @date 2024/3/26 13:13 6 | * 防止缓存穿透的默认值 7 | * 一旦从缓存中获取到的是这个值就直接返回 8 | */ 9 | public class RedisDefaultValue { 10 | 11 | public static final String REDIS_DEFAULT_VALUE = "redis_default_value"; 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/constant/RedisKeys.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.constant; 2 | 3 | /** 4 | * @author zzzi 5 | * @date 2024/3/25 13:03 6 | * 在这里设计所有的Redis中所有缓存的名称或者前缀 7 | */ 8 | public class RedisKeys { 9 | 10 | //按照视频发布时间降序存储视频的Zset的key 11 | public static final String VIDEO_FEED_PREFIX = "video:feed:"; 12 | 13 | //大V用户的id保存到一个Set中 14 | public static final String USER_HOT = "user:hot"; 15 | 16 | //缓存用户token的String的key的前缀 17 | public static final String USER_TOKEN_PREFIX = "user:token:"; 18 | 19 | //缓存用户信息的String的key的前缀 20 | public static final String USER_INFO_PREFIX = "user:info:"; 21 | 22 | //缓存视频信息的String的key的前缀 23 | public static final String VIDEO_INFO_PREFIX = "video:info:"; 24 | 25 | //用户对应的验证码的String的key前缀 26 | public static final String USER_VALID_CODE_PREFIX = "user:validCode:"; 27 | 28 | 29 | /** 30 | * @author zzzi 31 | * @date 2024/3/25 16:23 32 | * 下面是可选项 33 | */ 34 | //缓存用户所有作品的Set的key的前缀 35 | public static final String USER_WORKS_PREFIX = "user:works:"; 36 | 37 | //缓存用户所有点赞作品的Set的key的前缀 38 | public static final String USER_FAVORITES_PREFIX = "user:favorites:"; 39 | 40 | //缓存用户所有关注的Set的key的前缀 41 | public static final String USER_FOLLOWS_PREFIX = "user:follows:"; 42 | 43 | //缓存用户所有粉丝的Set的key的前缀 44 | public static final String USER_FOLLOWERS_PREFIX = "user:followers:"; 45 | 46 | //缓存视频所有评论的List的key的前缀 47 | public static final String VIDEO_COMMENTS_PREFIX = "video:comments:"; 48 | 49 | //互斥锁的前缀 50 | public static final String MUTEX_LOCK_PREFIX = "mutex_lock:"; 51 | } 52 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/CommentActionException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | public class CommentActionException extends RuntimeException { 4 | public CommentActionException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/CommentListException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | public class CommentListException extends RuntimeException { 4 | public CommentListException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/FollowException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | /**@author zzzi 4 | * @date 2024/3/30 15:17 5 | * 用户关注的异常 6 | */ 7 | public class FollowException extends RuntimeException{ 8 | public FollowException(String message) { 9 | super(message); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/RelationException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | /**@author zzzi 4 | * @date 2024/3/30 15:17 5 | * 用户关系的异常,好友列表,粉丝列表,关注列表 6 | */ 7 | public class RelationException extends RuntimeException{ 8 | public RelationException(String message) { 9 | super(message); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/UserException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | /**@author zzzi 4 | * @date 2024/3/26 21:33 5 | * 自定义的注册异常类,需要继承运行时异常 6 | */ 7 | public class UserException extends RuntimeException{ 8 | public UserException(String message) { 9 | super(message); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/UserInfoException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | public class UserInfoException extends RuntimeException{ 4 | public UserInfoException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/VideoException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | /**@author zzzi 4 | * @date 2024/3/27 14:59 5 | * 所有的视频模块异常使用这个类抛出 6 | */ 7 | public class VideoException extends RuntimeException{ 8 | public VideoException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/exception/VideoListException.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.exception; 2 | 3 | /**@author zzzi 4 | * @date 2024/3/27 14:59 5 | * 获取用户作品异常类 6 | */ 7 | public class VideoListException extends RuntimeException{ 8 | public VideoListException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/feign/UserClient.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.feign; 2 | 3 | 4 | import com.zzzi.common.feign.fallback.UserClientFallbackFactory; 5 | import com.zzzi.common.result.UserInfoVO; 6 | import com.zzzi.common.result.UserRelationListVO; 7 | import org.springframework.cloud.openfeign.FeignClient; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | 11 | //在这里指定好对应的微服务,之后就可以自动帮忙远程调用 12 | //要注意返回值和参数类型都必须一致 13 | 14 | /** 15 | * @author zzzi 16 | * @date 2024/4/2 18:17 17 | * Get请求必须添加@RequestParam,否则会报错Method has too many Body parameters 18 | * 因为远程调用无法保证在每个地方调用传递的参数名都能匹配上 19 | * 因为识别不到远程调用传递来的参数 20 | */ 21 | @FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class) 22 | public interface UserClient { 23 | //获取用户信息 24 | @GetMapping("/douyin/user/") 25 | UserInfoVO userInfo(@RequestParam("user_id") Long authorId); 26 | 27 | //获取用户关注列表 28 | @GetMapping("/douyin/relation/follow/list/") 29 | UserRelationListVO getFollowList(@RequestParam("user_id") String user_id, 30 | @RequestParam("token") String token); 31 | 32 | //获取用户粉丝列表 33 | @GetMapping("/douyin/relation/follower/list/") 34 | UserRelationListVO getFollowerList(@RequestParam("user_id") String user_id, 35 | @RequestParam("token") String token); 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/feign/fallback/UserClientFallbackFactory.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.feign.fallback; 2 | 3 | import com.zzzi.common.feign.UserClient; 4 | import com.zzzi.common.result.UserInfoVO; 5 | import com.zzzi.common.result.UserRelationListVO; 6 | import feign.hystrix.FallbackFactory; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | public class UserClientFallbackFactory implements FallbackFactory { 11 | //这里指定userClient访问失败时应该怎么做 12 | //这里返回了一个空的信息 13 | @Override 14 | public UserClient create(Throwable throwable) { 15 | return new UserClient() { 16 | 17 | @Override 18 | public UserInfoVO userInfo(Long id) { 19 | log.error("查询用户异常", throwable); 20 | return UserInfoVO.fail("查询用户信息异常"); 21 | } 22 | 23 | @Override 24 | public UserRelationListVO getFollowList(String user_id, String token) { 25 | log.error("查询关注列表异常", throwable); 26 | return UserRelationListVO.fail("查询关注列表异常"); 27 | } 28 | 29 | @Override 30 | public UserRelationListVO getFollowerList(String user_id, String token) { 31 | log.error("查询粉丝列表异常", throwable); 32 | return UserRelationListVO.fail("查询粉丝列表异常"); 33 | } 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/CommentActionVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author zzzi 10 | * @date 2024/4/2 22:22 11 | * 用户评论操作返回的实体类 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class CommentActionVO { 17 | private Integer status_code; 18 | private String status_msg; 19 | //当前用户评论的实体 20 | private CommentVO comment; 21 | 22 | public static CommentActionVO success(String status_msg, CommentVO comment) { 23 | return new CommentActionVO(0, status_msg, comment); 24 | } 25 | 26 | public static CommentActionVO fail(String status_msg) { 27 | return new CommentActionVO(-1, status_msg, null); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/CommentListVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author zzzi 11 | * @date 2024/4/2 22:22 12 | * 视频评论操作返回的实体类 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class CommentListVO { 18 | private Integer status_code; 19 | private String status_msg; 20 | private List comment_list; 21 | 22 | public static CommentListVO success(String status_msg, List comment_list) { 23 | return new CommentListVO(0, status_msg, comment_list); 24 | } 25 | 26 | public static CommentListVO fail(String status_msg) { 27 | return new CommentListVO(-1, status_msg, null); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/CommentVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import com.zzzi.common.result.UserVO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CommentVO { 13 | 14 | private Long id; 15 | //获取评论列表时,需要判断当前用户是不是我关注的 16 | private UserVO user; 17 | //评论内容 18 | private String content; 19 | //评论日期 20 | private String create_date; 21 | 22 | /** 23 | * @author zzzi 24 | * @date 2024/5/4 22:41 25 | * 前端判断isFather为true才解析son_list 26 | * 前端判断isFather为false才解析replyName 27 | */ 28 | private boolean is_father; 29 | private List son_list; 30 | private String replyName; 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Long id) { 37 | this.id = id; 38 | } 39 | 40 | public UserVO getUser() { 41 | return user; 42 | } 43 | 44 | public void setUser(UserVO user) { 45 | this.user = user; 46 | } 47 | 48 | public String getContent() { 49 | return content; 50 | } 51 | 52 | public void setContent(String content) { 53 | this.content = content; 54 | } 55 | 56 | public String getCreate_date() { 57 | return create_date; 58 | } 59 | 60 | public void setCreate_date(String create_date) { 61 | this.create_date = create_date; 62 | } 63 | 64 | public boolean getIs_father() { 65 | return is_father; 66 | } 67 | 68 | public void setIs_father(boolean father) { 69 | is_father = father; 70 | } 71 | 72 | public List getSon_list() { 73 | return son_list; 74 | } 75 | 76 | public void setSon_list(List son_list) { 77 | this.son_list = son_list; 78 | } 79 | 80 | public String getReplyName() { 81 | return replyName; 82 | } 83 | 84 | public void setReplyName(String replyName) { 85 | this.replyName = replyName; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/CommonVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @author zzzi 9 | * @date 2024/3/27 14:49 10 | * 通用的返回结果 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class CommonVO { 16 | private Integer status_code; 17 | private String status_msg; 18 | 19 | public static CommonVO success(String status_msg) { 20 | return new CommonVO(0, status_msg); 21 | } 22 | 23 | public static CommonVO fail(String status_msg) { 24 | return new CommonVO(-1, status_msg); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/MessageListVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | //好友之间的消息列表返回的实体类 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class MessageListVO { 14 | //状态码,0-成功,其他值-失败 15 | private Integer status_code; 16 | //状态信息 17 | private String status_msg; 18 | 19 | //当前用户的聊天记录 20 | List message_list; 21 | 22 | public static MessageListVO success(String status_msg, List message_list) { 23 | return new MessageListVO(0, status_msg, message_list); 24 | } 25 | 26 | public static MessageListVO fail(String status_msg) { 27 | return new MessageListVO(-1, status_msg, null); 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/MessageVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | //每一条消息对应的返回实体类 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class MessageVO { 13 | 14 | private Long id; 15 | //消息具体内容 16 | private String content; 17 | //这个封装的时候应该获取的是时间的毫秒值 18 | private Long create_time; 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/UserInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author zzzi 10 | * @date 2024/3/26 21:17 11 | * 查询用户所有信息时返回这个VO 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class UserInfoVO { 17 | //状态码,0-成功,其他值-失败 18 | private Integer status_code; 19 | //状态信息 20 | private String status_msg; 21 | //用户全部信息 22 | private UserVO user; 23 | 24 | 25 | //成功调用这个函数 26 | public static UserInfoVO success(UserVO user) { 27 | return new UserInfoVO(0, "成功", user); 28 | } 29 | 30 | //失败调用这个函数 31 | public static UserInfoVO fail(String status_msg) { 32 | return new UserInfoVO(-1, status_msg, null); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/UserRegisterLoginVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @author zzzi 9 | * @date 2024/3/26 21:17 10 | * 用户注册和登录返回这个VO对象 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class UserRegisterLoginVO { 16 | //状态码,0-成功,其他值-失败 17 | private Integer status_code; 18 | //状态信息 19 | private String status_msg; 20 | //用户id 21 | private Long user_id; 22 | //用户注册之后生成的token 23 | private String token; 24 | 25 | /** 26 | * @author zzzi 27 | * @date 2024/3/26 21:23 28 | * 向前端返回结果时调用这个函数即可 29 | */ 30 | public static UserRegisterLoginVO success(Long user_id, String token) { 31 | return new UserRegisterLoginVO(0, "成功", user_id, token); 32 | } 33 | 34 | public static UserRegisterLoginVO fail(String status_msg) { 35 | return new UserRegisterLoginVO(-1, status_msg, null, null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/UserRelationListVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author zzzi 11 | * @date 2024/3/29 22:16 12 | * 在这里定义用户关注列表、粉丝列表、好友列表的返回结果实体类 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class UserRelationListVO { 18 | //状态码,0-成功,其他值-失败 19 | private Integer status_code; 20 | //状态信息 21 | private String status_msg; 22 | //在这里封装用户列表 23 | List user_list; 24 | 25 | //成功返回这个结果 26 | //根据返回的字符串判断获取的是关注列表、粉丝列表还是好友列表 27 | public static UserRelationListVO success(String status_msg, List user_list) { 28 | return new UserRelationListVO(0, status_msg, user_list); 29 | } 30 | 31 | //失败调用这个函数 32 | public static UserRelationListVO fail(String status_msg) { 33 | return new UserRelationListVO(-1, status_msg, null); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/UserVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * @author zzzi 10 | * @date 2024/3/28 14:28 11 | * 这个类型多个模块都能用到,于是抽取到这里 12 | */ 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class UserVO { 16 | 17 | private Long id; 18 | private String name; 19 | private Integer follow_count; 20 | private Integer follower_count; 21 | /** 22 | * @author zzzi 23 | * @date 2024/4/1 13:49 24 | * 这个字段代表我是否关注了当前用户 25 | * 获取关注列表时,这个值肯定为true 26 | * 获取粉丝列表时,只有我也关注了他才会为true 27 | * 获取好友列表时,这个值肯定为true 28 | */ 29 | private boolean is_follow; 30 | //下面的字段是新增的 31 | private String avatar; 32 | private String background_image; 33 | private String signature; 34 | private Long total_favorited; 35 | private Integer work_count; 36 | private Integer favorite_count; 37 | 38 | 39 | /** 40 | * @author zzzi 41 | * @date 2024/3/27 13:33 42 | * 当某些字段Json转换出现问题时,主要原因就是set和get方法设置出现问题 43 | */ 44 | public Long getId() { 45 | return id; 46 | } 47 | 48 | public void setId(Long id) { 49 | this.id = id; 50 | } 51 | 52 | public String getName() { 53 | return name; 54 | } 55 | 56 | public void setName(String name) { 57 | this.name = name; 58 | } 59 | 60 | public Integer getFollow_count() { 61 | return follow_count; 62 | } 63 | 64 | public void setFollow_count(Integer follow_count) { 65 | this.follow_count = follow_count; 66 | } 67 | 68 | public Integer getFollower_count() { 69 | return follower_count; 70 | } 71 | 72 | public void setFollower_count(Integer follower_count) { 73 | this.follower_count = follower_count; 74 | } 75 | 76 | public boolean getIs_follow() { 77 | return is_follow; 78 | } 79 | 80 | public void setIs_follow(boolean is_follow) { 81 | this.is_follow = is_follow; 82 | } 83 | 84 | public String getAvatar() { 85 | return avatar; 86 | } 87 | 88 | public void setAvatar(String avatar) { 89 | this.avatar = avatar; 90 | } 91 | 92 | public String getBackground_image() { 93 | return background_image; 94 | } 95 | 96 | public void setBackground_image(String background_image) { 97 | this.background_image = background_image; 98 | } 99 | 100 | public String getSignature() { 101 | return signature; 102 | } 103 | 104 | public void setSignature(String signature) { 105 | this.signature = signature; 106 | } 107 | 108 | public Long getTotal_favorited() { 109 | return total_favorited; 110 | } 111 | 112 | public void setTotal_favorited(Long total_favorited) { 113 | this.total_favorited = total_favorited; 114 | } 115 | 116 | public Integer getWork_count() { 117 | return work_count; 118 | } 119 | 120 | public void setWork_count(Integer work_count) { 121 | this.work_count = work_count; 122 | } 123 | 124 | public Integer getFavorite_count() { 125 | return favorite_count; 126 | } 127 | 128 | public void setFavorite_count(Integer favorite_count) { 129 | this.favorite_count = favorite_count; 130 | } 131 | 132 | /** 133 | * @author zzzi 134 | * @date 2024/4/1 14:27 135 | * 因为要比较两个UserVO是否相等,所以需要重写equals和hashCode方法 136 | * 比较时id一样,就说明两个实体是相等的,因为id都是唯一的 137 | */ 138 | @Override 139 | public boolean equals(Object o) { 140 | if (this == o) return true; 141 | //这里可以判断当前对象是不是UserVO类,这样增加equals方法的健壮性 142 | if (!(o instanceof UserVO)) 143 | return false; 144 | UserVO userVO = (UserVO) o; 145 | if (userVO == null || getClass() != userVO.getClass()) return false; 146 | return id.equals(userVO.id); 147 | } 148 | 149 | @Override 150 | public int hashCode() { 151 | return Objects.hash(id); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/VideoFeedListVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class VideoFeedListVO { 13 | private Integer status_code; 14 | private String status_msg; 15 | /** 16 | * @author zzzi 17 | * @date 2024/3/29 12:20 18 | * 返回结果时告知下次推荐视频的时间节点 19 | * 防止刷到旧视频 20 | */ 21 | private Long next_time; 22 | List video_list; 23 | 24 | //成功调用这个函数 25 | public static VideoFeedListVO success(String status_msg, Long next_time, List video_list) { 26 | return new VideoFeedListVO(0, status_msg, next_time, video_list); 27 | } 28 | 29 | //失败调用这个函数 30 | public static VideoFeedListVO fail(String status_msg) { 31 | return new VideoFeedListVO(-1, status_msg, null, null); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/VideoListVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.bytedeco.javacpp.annotation.NoDeallocator; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @NoDeallocator 11 | @AllArgsConstructor 12 | public class VideoListVO { 13 | private Integer status_code; 14 | private String status_msg; 15 | 16 | List video_list; 17 | 18 | //成功调用这个函数 19 | public static VideoListVO success(String status_msg, List video_list) { 20 | return new VideoListVO(0, status_msg, video_list); 21 | } 22 | 23 | //失败调用这个函数 24 | public static VideoListVO fail(String status_msg) { 25 | return new VideoListVO(-1, status_msg, null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/result/VideoVO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.result; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.Objects; 7 | 8 | /**@author zzzi 9 | * @date 2024/3/28 14:26 10 | * 每一个视频都对应这样一个实体,视频列表中的每一项都是这样一个对象 11 | */ 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class VideoVO { 15 | private Long id; 16 | private UserVO author; 17 | private String play_url; 18 | private String cover_url; 19 | private Integer favorite_count; 20 | private Integer comment_count; 21 | private boolean is_favorite; 22 | private String title; 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public void setId(Long id) { 29 | this.id = id; 30 | } 31 | 32 | public UserVO getAuthor() { 33 | return author; 34 | } 35 | 36 | public void setAuthor(UserVO author) { 37 | this.author = author; 38 | } 39 | 40 | public String getPlay_url() { 41 | return play_url; 42 | } 43 | 44 | public void setPlay_url(String play_url) { 45 | this.play_url = play_url; 46 | } 47 | 48 | public String getCover_url() { 49 | return cover_url; 50 | } 51 | 52 | public void setCover_url(String cover_url) { 53 | this.cover_url = cover_url; 54 | } 55 | 56 | public Integer getFavorite_count() { 57 | return favorite_count; 58 | } 59 | 60 | public void setFavorite_count(Integer favorite_count) { 61 | this.favorite_count = favorite_count; 62 | } 63 | 64 | public Integer getComment_count() { 65 | return comment_count; 66 | } 67 | 68 | public void setComment_count(Integer comment_count) { 69 | this.comment_count = comment_count; 70 | } 71 | 72 | public boolean getIs_favorite() { 73 | return is_favorite; 74 | } 75 | 76 | public void setIs_favorite(boolean is_favorite) { 77 | this.is_favorite = is_favorite; 78 | } 79 | 80 | public String getTitle() { 81 | return title; 82 | } 83 | 84 | public void setTitle(String title) { 85 | this.title = title; 86 | } 87 | 88 | /**@author zzzi 89 | * @date 2024/4/2 18:08 90 | * 由于要进行比较,所以要重写equals和hashCode 91 | */ 92 | @Override 93 | public boolean equals(Object o) { 94 | if (this == o) return true; 95 | if (o == null || getClass() != o.getClass()) return false; 96 | VideoVO videoVO = (VideoVO) o; 97 | return id.equals(videoVO.id); 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | return Objects.hash(id); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/COSUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Data 8 | @Component 9 | @ConfigurationProperties(prefix = "tencent.cos") 10 | public class COSUtils { 11 | 12 | private String secretId; 13 | 14 | private String secretKey; 15 | 16 | private String bucketName; 17 | 18 | private String region; 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/JwtUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import io.jsonwebtoken.*; 4 | import org.springframework.util.StringUtils; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * @author zzzi 10 | * @date 2024/3/25 18:56 11 | * 根据用户名和用户id生成用户的token 12 | */ 13 | public class JwtUtils { 14 | //过期时间 ms (1 day) 15 | private static long tokenExpiration = 24 * 60 * 60 * 1000; 16 | //签名密钥 17 | private static String tokenSignKey = "9q8w5sad65sca54fas65"; 18 | 19 | //生成token 20 | 21 | /** 22 | * @author zzzi 23 | * @date 2024/3/25 12:58 24 | * 只要用户id和用户名一致,前后的token就是一致的 25 | */ 26 | public static String createToken(Long userId, String userName) { 27 | String token = Jwts.builder() 28 | .setSubject("DY-USER") 29 | .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) 30 | .claim("userId", userId) 31 | .claim("userName", userName) 32 | .signWith(SignatureAlgorithm.HS512, tokenSignKey) 33 | .compressWith(CompressionCodecs.GZIP) 34 | .compact(); 35 | return token; 36 | } 37 | 38 | //根据token字符串得到用户id 39 | public static Long getUserIdByToken(String token) { 40 | if (StringUtils.isEmpty(token)) return null; 41 | Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); 42 | Claims claims = claimsJws.getBody(); 43 | Long userId = (Long) claims.get("userId"); 44 | return userId.longValue(); 45 | } 46 | 47 | //根据token字符串得到用户名称 48 | public static String getUserNameByToken(String token) { 49 | if (StringUtils.isEmpty(token)) return ""; 50 | Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); 51 | Claims claims = claimsJws.getBody(); 52 | return (String) claims.get("userName"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/MD5Utils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | /** 7 | * @author zzzi 8 | * @date 2024/3/25 18:57 9 | * 将密码使用MD5加密,保证数据的安全性 10 | * 查询时先将密码加密,然后传入数据库比对 11 | */ 12 | public class MD5Utils { 13 | //定义一个随机的盐值 14 | public static final String SALT = "fdfa5e5a88bebae640a5d88e7c84708"; 15 | 16 | public static String parseStrToMd5L32(String str) { 17 | // 将字符串转换为32位小写MD5 18 | String reStr = null; 19 | try { 20 | str += SALT; 21 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 22 | //得到加密后的值 23 | byte[] bytes = md5.digest(str.getBytes()); 24 | StringBuffer stringBuffer = new StringBuffer(); 25 | for (byte b : bytes) { 26 | //转换成无符号整型 27 | int bt = b & 0xff; 28 | if (bt < 16) { 29 | stringBuffer.append(0); 30 | } 31 | stringBuffer.append(Integer.toHexString(bt)); 32 | } 33 | reStr = stringBuffer.toString(); 34 | } catch (NoSuchAlgorithmException e) { 35 | e.printStackTrace(); 36 | } 37 | return reStr; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/MinioUploadUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import io.minio.MinioClient; 4 | import io.minio.PutObjectOptions; 5 | import io.minio.errors.InvalidEndpointException; 6 | import io.minio.errors.InvalidPortException; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.time.LocalDateTime; 16 | import java.util.UUID; 17 | 18 | @Slf4j 19 | @Component 20 | public class MinioUploadUtils { 21 | @Autowired 22 | private MinioUtils minioUtils; 23 | 24 | public String upload(File file, String suffix) throws InvalidPortException, InvalidEndpointException, IOException { 25 | // 初始化minio客户端 26 | MinioClient client = new MinioClient(minioUtils.getEndPoint(), minioUtils.getAccessKey(), minioUtils.getSecretKey()); 27 | //获取文件输入流 28 | InputStream inputStream = new FileInputStream(file); 29 | try { 30 | // 生成唯一文件名,当前时间 +UUID+ 文件类型 31 | //指定文件保存的路径为存储桶下面的tiktok/文件夹下 32 | String choice = suffix.equals("_cover.jpg") ? "cover/" : "video/"; 33 | String fileName = "tiktok/" + choice + LocalDateTime.now() + UUID.randomUUID() + suffix; 34 | 35 | //上传文件 36 | client.putObject(minioUtils.getBucketName(), fileName, inputStream, new PutObjectOptions(inputStream.available(), -1)); 37 | 38 | //返回上传的URL路径 39 | String url = client.getObjectUrl("tiktok", fileName); //文件访问路径 40 | return url; 41 | } catch (Exception e) { 42 | throw new RuntimeException("上传失败"); 43 | } finally { 44 | inputStream.close(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/MinioUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Data 8 | @Component 9 | @ConfigurationProperties(prefix = "minio") 10 | public class MinioUtils { 11 | private String endPoint; 12 | private String accessKey; 13 | private String secretKey; 14 | private String bucketName; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/RandomUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import lombok.Data; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.concurrent.ThreadLocalRandom; 10 | 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/3/30 14:40 15 | * 在这里生成一个给定范围的随机值用来将缓存的过期时间随机打散 16 | * 防止缓存雪崩 17 | */ 18 | @Slf4j 19 | @Component 20 | public class RandomUtils { 21 | @Value("${random_start}") 22 | private int start; 23 | @Value("${random_end}") 24 | private int end; 25 | 26 | /** 27 | * @author zzzi 28 | * @date 2024/3/30 14:45 29 | * 返回一个随机的过期值,有最小边界 30 | * 使用ThreadLocalRandom是为了防止多线程下由于竞争导致的效率问题 31 | */ 32 | public Integer createRandomTime() { 33 | if (start == end) { 34 | int time = ThreadLocalRandom.current().nextInt(start, end + 31); 35 | log.info("生成随机的缓存过期时间为:{}", time); 36 | return time; 37 | } 38 | int time = ThreadLocalRandom.current().nextInt(start, end + 1); 39 | log.info("生成随机的缓存过期时间为:{}", time); 40 | return time; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/RedisLockUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import cn.hutool.core.lang.UUID; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.data.redis.core.script.DefaultRedisScript; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.Collections; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * @author zzzi 16 | * @date 2024/4/12 15:14 17 | * 在这里实现Redis的分布式可重入锁 18 | */ 19 | @Component 20 | @Slf4j 21 | public class RedisLockUtils { 22 | 23 | @Resource 24 | private StringRedisTemplate redisTemplate; 25 | /** 26 | * @author zzzi 27 | * @date 2024/4/12 15:24 28 | * 每一个JVM对应一个独立的UUID 29 | * 因为他只会初始化一次,是一个常量 30 | * 使用这个的目的是为了解决集群中不同JVM内部可能出现线程id重复的问题 31 | * key-filed-value 32 | */ 33 | private static final String VALUE_PREFIX = UUID.randomUUID().toString(true) + "-"; 34 | private static final DefaultRedisScript REENTRANT_LOCK; 35 | private static final DefaultRedisScript UN_LOCK; 36 | private static final DefaultRedisScript REENTRANT_UN_LOCK; 37 | 38 | static { 39 | REENTRANT_LOCK = new DefaultRedisScript<>(); 40 | REENTRANT_LOCK.setLocation(new ClassPathResource("reentrantLock.lua")); 41 | REENTRANT_UN_LOCK = new DefaultRedisScript<>(); 42 | REENTRANT_UN_LOCK.setLocation(new ClassPathResource("reentrantUnLock.lua")); 43 | 44 | UN_LOCK = new DefaultRedisScript<>(); 45 | UN_LOCK.setLocation(new ClassPathResource("unLock.lua")); 46 | } 47 | 48 | /** 49 | * @author zzzi 50 | * @date 2024/4/12 22:31 51 | * 可重入锁加锁 52 | */ 53 | public boolean reentrantLock(String key, long time) { 54 | log.info("给:{}加锁", key); 55 | //获取当前线程id 56 | long currentThreadId = Thread.currentThread().getId(); 57 | 58 | //加锁时防止不同JVM中线程id一样加锁失败或者误删锁 59 | Long execute = redisTemplate.execute( 60 | REENTRANT_LOCK, 61 | Collections.singletonList(key), 62 | VALUE_PREFIX + currentThreadId, 63 | time * 60 * 1000 64 | ); 65 | //根据返回值是不是1判断执行情况 66 | return execute == 1; 67 | 68 | } 69 | 70 | /** 71 | * @author zzzi 72 | * @date 2024/4/12 22:33 73 | * 可重入锁解锁 74 | */ 75 | public void reentrantUnLock(String key, long time) { 76 | log.info("给:{}解锁", key); 77 | //获取当前线程id 78 | long currentThreadId = Thread.currentThread().getId(); 79 | 80 | //加锁时防止不同JVM中线程id一样误删锁 81 | redisTemplate.execute( 82 | REENTRANT_UN_LOCK, 83 | Collections.singletonList(key), 84 | VALUE_PREFIX + currentThreadId, 85 | time * 60 * 1000 86 | ); 87 | } 88 | 89 | /** 90 | * @author zzzi 91 | * @date 2024/4/12 15:23 92 | * 加锁操作,由于key是固定的,所以这里加锁解锁都是可行的 93 | * 生成uuid的目的是为了防止线程id重复误删锁 94 | */ 95 | public boolean lock(String key, long time, TimeUnit timeUnit) { 96 | log.info("给:{}加锁", key); 97 | //获取当前线程id 98 | long currentThreadId = Thread.currentThread().getId(); 99 | 100 | //加锁时防止不同JVM中线程id一样加锁失败或者误删锁 101 | Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, VALUE_PREFIX + currentThreadId, time, timeUnit); 102 | return Boolean.TRUE.equals(absent); 103 | } 104 | 105 | /** 106 | * @author zzzi 107 | * @date 2024/4/12 15:25 108 | * 解锁操作 109 | * 生成整个JVM内唯一的UUID是为了防止集群模式下不同的线程id重复导致误删锁 110 | * 使用lua脚本保证解锁的原子性 111 | */ 112 | public void unLock(String key) { 113 | log.info("给:{}解锁", key); 114 | //获取当前线程id 115 | long currentThreadId = Thread.currentThread().getId(); 116 | //加锁时防止不同JVM中线程id一样误删锁 117 | redisTemplate.execute( 118 | UN_LOCK, 119 | Collections.singletonList(key), 120 | VALUE_PREFIX + currentThreadId 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/SMSUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author zzzi 9 | * @date 2024/4/14 15:27 10 | * 腾讯云短信服务配置类 11 | */ 12 | @Data 13 | @Component 14 | @ConfigurationProperties(prefix = "tencent.sms") 15 | public class SMSUtils { 16 | 17 | private String secretId; 18 | private String secretKey; 19 | private String endpoint; 20 | private String region; 21 | private String sdkAppId; 22 | private String signName; 23 | private String templateId; 24 | private String signMethod; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/SendMessageUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import com.tencentcloudapi.common.Credential; 4 | import com.tencentcloudapi.common.exception.TencentCloudSDKException; 5 | import com.tencentcloudapi.common.profile.ClientProfile; 6 | import com.tencentcloudapi.common.profile.HttpProfile; 7 | import com.tencentcloudapi.sms.v20190711.SmsClient; 8 | import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest; 9 | import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.Random; 15 | 16 | /** 17 | * @author zzzi 18 | * @date 2024/4/14 15:31 19 | * 腾讯云SMS发送短信工具类 20 | */ 21 | @Slf4j 22 | @Component 23 | public class SendMessageUtils { 24 | @Autowired 25 | private SMSUtils smsUtils; 26 | 27 | //发送短信 28 | public boolean sendMessage(String phoneNum, String validCode, String expireTime) { 29 | try { 30 | //实例化认证对象 31 | Credential cred = new Credential(smsUtils.getSecretId(), smsUtils.getSecretKey()); 32 | 33 | // 实例化一个http选项 34 | HttpProfile httpProfile = new HttpProfile(); 35 | httpProfile.setReqMethod("POST"); 36 | //超时时间 37 | httpProfile.setConnTimeout(60); 38 | //指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com 39 | httpProfile.setEndpoint(smsUtils.getEndpoint()); 40 | 41 | /* 非必要步骤: 42 | * 实例化一个客户端配置对象,可以指定超时时间等配置 */ 43 | ClientProfile clientProfile = new ClientProfile(); 44 | //签名加密算法 45 | clientProfile.setSignMethod(smsUtils.getSignMethod()); 46 | clientProfile.setHttpProfile(httpProfile); 47 | SmsClient client = new SmsClient(cred, smsUtils.getRegion(), clientProfile); 48 | 49 | //实例化请求对象 50 | SendSmsRequest req = new SendSmsRequest(); 51 | 52 | //设置短信应用ID 53 | String sdkAppId = smsUtils.getSdkAppId(); 54 | req.setSmsSdkAppid(sdkAppId); 55 | 56 | //设置短信签名 57 | String signName = smsUtils.getSignName(); 58 | req.setSign(signName); 59 | 60 | //短信模版ID 61 | String templateId = smsUtils.getTemplateId(); 62 | req.setTemplateID(templateId); 63 | 64 | //设置模版中的参数,这里是具体的短信验证码和短信验证码的到期时间 65 | req.setTemplateParamSet(new String[]{validCode, expireTime}); 66 | 67 | //设置要发送的手机号 68 | req.setPhoneNumberSet(new String[]{phoneNum}); 69 | 70 | //发送短信并得到发送的响应结果 71 | SendSmsResponse res = client.SendSms(req); 72 | //查看结果结构 73 | log.info("短信发送结果的结构为:{}", SendSmsResponse.toJsonString(res)); 74 | 75 | //由于一次只发送一条短信,所以只用拿到SendStatus中的第一个状态即可 76 | String resCode = res.getSendStatusSet()[0].getCode(); 77 | return "Ok".equals(resCode); 78 | 79 | } catch (TencentCloudSDKException e) { 80 | log.error("给:{}发送短信失败", phoneNum); 81 | e.printStackTrace(); 82 | } 83 | //这一步只是为了不报错,实际不会执行 84 | return false; 85 | } 86 | 87 | //生成位随机验证码,可以六位可以四位 88 | public String geneValidCode() { 89 | Random random = new Random(); 90 | return (random.nextInt(900001) + 100000) + ""; 91 | //return (random.nextInt(9001) + 1000) + ""; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/UpdateTokenUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | 4 | import com.zzzi.common.constant.RedisKeys; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/4/2 16:21 15 | * 在这里更新用户token的过期时间 16 | */ 17 | @Slf4j 18 | @Component 19 | public class UpdateTokenUtils { 20 | @Autowired 21 | private RandomUtils randomUtils; 22 | @Autowired 23 | private StringRedisTemplate redisTemplate; 24 | 25 | public void updateTokenExpireTimeUtils(String userId) { 26 | Integer userTokenExpireTime = randomUtils.createRandomTime(); 27 | redisTemplate.expire(RedisKeys.USER_TOKEN_PREFIX + userId, userTokenExpireTime, TimeUnit.MINUTES); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/UpdateUserInfoUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import com.zzzi.common.constant.RedisKeys; 4 | import com.zzzi.common.exception.UserException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.aop.framework.AopContext; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * @author zzzi 15 | * @date 2024/3/29 21:40 16 | * 在这里统一更新用户的缓存 17 | */ 18 | @Component 19 | @Slf4j 20 | public class UpdateUserInfoUtils { 21 | 22 | @Autowired 23 | private StringRedisTemplate redisTemplate; 24 | @Autowired 25 | private UpdateUserInfoUtils updateUserInfoUtils; 26 | 27 | /** 28 | * @author zzzi 29 | * @date 2024/3/31 15:44 30 | * 先更新数据库再更新缓存 31 | */ 32 | public void updateUserInfoCache(Long authorId, String userDOJson) { 33 | //todo 实现AP模式,牺牲一致性,拿到的可能是旧数据,但是保证业务可用 34 | String mutex = MD5Utils.parseStrToMd5L32(userDOJson); 35 | try { 36 | //拿到互斥锁 37 | /**@author zzzi 38 | * @date 2024/3/31 15:37 39 | * 当前线程加上互斥锁,防止大量重建请求同时到达数据库造成数据库压力过大:解决缓存击穿 40 | * 加上锁之后只有一个线程缓存重建 41 | * 同时设置用户信息不过期,进一步防止缓存击穿 42 | */ 43 | long currentThreadId = Thread.currentThread().getId(); 44 | Boolean absent = redisTemplate.opsForValue().setIfAbsent(RedisKeys.MUTEX_LOCK_PREFIX + mutex, currentThreadId + "", 1, TimeUnit.MINUTES); 45 | 46 | //没拿到互斥锁说明当前用户正在被修改,应该重试 47 | if (!absent) { 48 | Thread.sleep(50); 49 | //调用自己重试,防止事务失效,这里要注入自己 50 | updateUserInfoUtils.updateUserInfoCache(authorId, userDOJson); 51 | } 52 | 53 | /**@author zzzi 54 | * @date 2024/3/29 13:24 55 | * 更新用户缓存,直接覆盖旧值 56 | */ 57 | redisTemplate.opsForValue().set(RedisKeys.USER_INFO_PREFIX + authorId, userDOJson); 58 | 59 | } catch (Exception e) { 60 | log.error(e.getMessage()); 61 | //手动回滚 62 | throw new UserException("更新用户信息失败"); 63 | } finally {//最后释放互斥锁 64 | /**@author zzzi 65 | * @date 2024/3/31 15:37 66 | * 需要是加锁的线程才能解锁 67 | */ 68 | String currentThreadId = Thread.currentThread().getId() + ""; 69 | String threadId = redisTemplate.opsForValue().get(RedisKeys.MUTEX_LOCK_PREFIX + mutex); 70 | //加锁的就是当前线程才解锁 71 | if (currentThreadId.equals(threadId)) { 72 | redisTemplate.delete(RedisKeys.MUTEX_LOCK_PREFIX + mutex); 73 | } 74 | } 75 | } 76 | 77 | public void deleteHotUserFormCache(Long userId) { 78 | try { 79 | //先针对当前热点用户的缓存加锁 80 | //加上互斥锁 81 | long currentThreadId = Thread.currentThread().getId(); 82 | Boolean absent = redisTemplate.opsForValue(). 83 | setIfAbsent(RedisKeys.USER_HOT + "_mutex", currentThreadId + "", 1, TimeUnit.MINUTES); 84 | if (!absent) { 85 | Thread.sleep(50); 86 | UpdateUserInfoUtils updateUserInfoUtils = (UpdateUserInfoUtils) AopContext.currentProxy(); 87 | updateUserInfoUtils.deleteHotUserFormCache(userId); 88 | } 89 | //到这里获取到锁,然后删除这个大V 90 | //传递String[]数组会更加好 91 | redisTemplate.opsForSet().remove(RedisKeys.USER_HOT, new String[]{userId.toString()}); 92 | } catch (Exception e) { 93 | log.error("删除热点用户:{}s失败", userId); 94 | throw new RuntimeException("删除热点用户失败"); 95 | } finally { 96 | //最后需要删除互斥锁 97 | String currentThreadId = Thread.currentThread().getId() + ""; 98 | String threadId = redisTemplate.opsForValue().get(RedisKeys.USER_HOT + "_mutex"); 99 | //加锁的就是当前线程才解锁 100 | if (currentThreadId.equals(threadId)) { 101 | redisTemplate.delete(RedisKeys.USER_HOT + "_mutex"); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/UpdateVideoInfoUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import com.zzzi.common.constant.RedisKeys; 4 | import com.zzzi.common.exception.VideoException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/3/29 21:40 15 | * 在这里统一更新视频信息的缓存 16 | */ 17 | @Component 18 | @Slf4j 19 | public class UpdateVideoInfoUtils { 20 | 21 | @Autowired 22 | private StringRedisTemplate redisTemplate; 23 | @Autowired 24 | private UpdateVideoInfoUtils updateUserInfoUtils; 25 | 26 | /** 27 | * @author zzzi 28 | * @date 2024/3/31 15:44 29 | * 当前线程加上互斥锁,防止大量重建请求同时到达数据库造成数据库压力过大:解决缓存击穿 30 | * 同时设置视频信息不过期,进一步防止缓存击穿 31 | */ 32 | //todo 实现AP模式,牺牲一致性,拿到的可能是旧数据,但是保证业务可用 33 | public void updateVideoInfoCache(Long videoId, String videoDOJson) { 34 | String mutex = MD5Utils.parseStrToMd5L32(videoDOJson); 35 | try { 36 | //拿到互斥锁 37 | /**@author zzzi 38 | * @date 2024/3/31 15:37 39 | * 当前线程加上互斥锁 40 | */ 41 | long currentThreadId = Thread.currentThread().getId(); 42 | Boolean absent = redisTemplate.opsForValue().setIfAbsent(RedisKeys.MUTEX_LOCK_PREFIX + mutex, currentThreadId + "", 1, TimeUnit.MINUTES); 43 | 44 | //没拿到互斥锁说明当前视频正在被修改,应该重试 45 | if (!absent) { 46 | Thread.sleep(50); 47 | //调用自己重试,防止事务失效,这里要注入自己 48 | updateUserInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 49 | } 50 | 51 | /**@author zzzi 52 | * @date 2024/3/29 13:24 53 | * 更新视频缓存,直接覆盖旧值 54 | */ 55 | redisTemplate.opsForValue().set(RedisKeys.VIDEO_INFO_PREFIX + videoId, videoDOJson); 56 | 57 | } catch (Exception e) { 58 | log.error(e.getMessage()); 59 | //手动回滚 60 | throw new VideoException("更新视频信息失败"); 61 | } finally {//最后释放互斥锁 62 | /**@author zzzi 63 | * @date 2024/3/31 15:37 64 | * 需要是加锁的线程才能解锁 65 | */ 66 | String currentThreadId = Thread.currentThread().getId() + ""; 67 | String threadId = redisTemplate.opsForValue().get(RedisKeys.MUTEX_LOCK_PREFIX + mutex); 68 | //加锁的就是当前线程才解锁 69 | if (currentThreadId.equals(threadId)) { 70 | redisTemplate.delete(RedisKeys.MUTEX_LOCK_PREFIX + mutex); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/UploadUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import com.qcloud.cos.COSClient; 4 | import com.qcloud.cos.ClientConfig; 5 | import com.qcloud.cos.auth.BasicCOSCredentials; 6 | import com.qcloud.cos.auth.COSCredentials; 7 | import com.qcloud.cos.model.PutObjectRequest; 8 | import com.qcloud.cos.region.Region; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.File; 14 | import java.time.LocalDateTime; 15 | import java.util.UUID; 16 | 17 | @Slf4j 18 | @Component 19 | public class UploadUtils { 20 | 21 | @Autowired 22 | private COSUtils cosUtils; 23 | 24 | /** 25 | * @author zzzi 26 | * @date 2024/3/23 21:56 27 | * 根据传递而来的文件以及对应的后缀将文件上传到对应的文件夹中 28 | * 后缀为.jpg就是上传cover,否则就是上传视频 29 | */ 30 | public String upload(File file, String suffix) { 31 | // 初始化cos客户端 32 | COSCredentials cred = new BasicCOSCredentials(cosUtils.getSecretId(), cosUtils.getSecretKey()); 33 | ClientConfig clientConfig = new ClientConfig(new Region(cosUtils.getRegion())); 34 | COSClient cosClient = new COSClient(cred, clientConfig); 35 | try { 36 | // 生成唯一文件名,当前时间 +UUID+ 文件类型 37 | //指定文件保存的路径为存储桶下面的tiktok/文件夹下 38 | String choice = suffix.equals("_cover.jpg") ? "cover/" : "video/"; 39 | /**@author zzzi 40 | * @date 2024/4/9 13:38 41 | * 这个文件名直接决定了文件上传到对应的桶中的文件名 42 | * 自己确定文件名,然后调用腾讯云cos接口,之后自己拼接文件的访问路径 43 | */ 44 | String fileName = "tiktok/" + choice + LocalDateTime.now() + UUID.randomUUID() + suffix; 45 | 46 | // 上传文件到cos,上传的核心步骤 47 | PutObjectRequest putObjectRequest = new PutObjectRequest(cosUtils.getBucketName(), fileName, file); 48 | cosClient.putObject(putObjectRequest); 49 | // 返回文件在cos上的访问url,直接拼接起来 50 | //https://zzzi-img-1313100942.cos.ap-beijing.myqcloud.com/tiktok/video/2024-03-24T10:28:33.668779d646a-8c32-4c53-b6df-23958a72cd31_video.mp4 51 | String url = "https://" + cosUtils.getBucketName() + ".cos." + cosUtils.getRegion() + ".myqcloud.com/" + fileName; 52 | 53 | //返回上传的URL路径 54 | return url; 55 | } catch (Exception e) { 56 | throw new RuntimeException("上传失败"); 57 | } finally { 58 | // 关闭cos客户端 59 | cosClient.shutdown(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/src/main/java/com/zzzi/common/utils/VideoUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.common.utils; 2 | 3 | import org.bytedeco.javacv.FFmpegFrameGrabber; 4 | import org.bytedeco.javacv.Frame; 5 | import org.bytedeco.javacv.FrameGrabber; 6 | import org.bytedeco.javacv.Java2DFrameConverter; 7 | 8 | import javax.imageio.ImageIO; 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.io.File; 12 | 13 | /** 14 | * @author zzzi 15 | * @date 2024/3/23 21:32 16 | * 根据传递而来的视频抽取一阵作为封面并返回 17 | */ 18 | 19 | public class VideoUtils { 20 | 21 | /** 22 | * @author zzzi 23 | * @date 2024/3/24 10:30 24 | * 抓取视频帧作为封面,保存到本地 25 | * 返回对应的封面文件 26 | */ 27 | public static File fetchPic(File file, String dirName, String coverName) { 28 | try { 29 | FFmpegFrameGrabber ff = new FFmpegFrameGrabber(file); 30 | ff.start(); 31 | int length = ff.getLengthInFrames(); 32 | 33 | //保存封面的路径不存在时需要新建 34 | File cover_dir = new File(dirName); 35 | if (!cover_dir.exists()) { 36 | cover_dir.mkdirs(); 37 | } 38 | File targetFile = new File(cover_dir, coverName); 39 | 40 | String frameFile = dirName + coverName; 41 | int i = 0; 42 | Frame frame = null; 43 | while (i < length) { 44 | // 过滤前5帧,避免出现全黑的图片,依自己情况而定 45 | 46 | frame = ff.grabFrame(); 47 | //找到了合适的帧 48 | if ((i > 5) && (frame.image != null)) { 49 | break; 50 | } 51 | i++; 52 | } 53 | 54 | //获取文件格式:.jpg、.png 55 | String imgSuffix = "jpg"; 56 | if (frameFile.indexOf('.') != -1) { 57 | String[] arr = frameFile.split("\\."); 58 | if (arr.length >= 2) { 59 | imgSuffix = arr[1]; 60 | } 61 | } 62 | 63 | Java2DFrameConverter converter = new Java2DFrameConverter(); 64 | BufferedImage srcBi = converter.getBufferedImage(frame); 65 | int owidth = srcBi.getWidth(); 66 | int oheight = srcBi.getHeight(); 67 | // 对截取的帧进行等比例缩放 68 | int width = 800; 69 | int height = (int) (((double) width / owidth) * oheight); 70 | BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 71 | bi.getGraphics().drawImage(srcBi.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null); 72 | try { 73 | ImageIO.write(bi, imgSuffix, targetFile); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | ff.stop(); 78 | return targetFile; 79 | } catch (FrameGrabber.Exception e) { 80 | e.printStackTrace(); 81 | } 82 | return null; 83 | } 84 | } -------------------------------------------------------------------------------- /common/src/main/resources/reentrantLock.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by Luanalysis 3 | --- Created by zzzi. 4 | --- DateTime: 2024/4/12 22:12 5 | --- 6 | local key = KEYS[1]; -- 锁的key 7 | local threadId = ARGV[1]; --线程唯一id 8 | local releaseTime = ARGV[2] --锁的过期时间 9 | 10 | -- 判断锁是否存在 11 | if (redis.call('exists', key) == 0) then 12 | -- 不存在直接加锁 13 | redis.call('hset', key, threadId, '1'); 14 | -- 设置锁的有效期 15 | redis.call('expire', key, releaseTime); 16 | return 1; 17 | end ; 18 | 19 | --锁已经存在,判断是不是自己加的锁,实现可重入 20 | if (redis.call('hexists', key, threadId) == 1) then 21 | -- 到这里说明是自己加的锁,重入次数+1 22 | redis.call('hincrby', key, threadId, '1'); 23 | --重新设置锁的有效期 24 | redis.call('expire', key, releaseTime); 25 | return 1; 26 | end ; 27 | -- 加锁失败返回0 28 | return 0; -------------------------------------------------------------------------------- /common/src/main/resources/reentrantUnLock.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by Luanalysis 3 | --- Created by zzzi. 4 | --- DateTime: 2024/4/12 22:08 5 | --- 6 | local key = KEYS[1]; -- 锁的key 7 | local threadId = ARGV[1]; --线程唯一id 8 | local releaseTime = ARGV[2] --锁的过期时间 9 | 10 | --判断当前锁是不是自己加的 11 | if (redis.call('hexists', key, threadId) == 0) then 12 | return 0; 13 | end ; 14 | 15 | --到这里说明是自己的锁,重入次数-1 16 | local count = redis.call('hincrby', key, threadId, -1); 17 | 18 | --重入次数为0才删除锁 19 | if (count > 0) then 20 | -- 重入次数不为0,更新锁的有效时间 21 | redis.call('expire', key, releaseTime); 22 | return 1; 23 | else 24 | -- 重入次数为0删除锁 25 | redis.call('del', key); 26 | return 1; 27 | end ; 28 | 29 | -------------------------------------------------------------------------------- /common/src/main/resources/unLock.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by Luanalysis 3 | --- Created by zzzi. 4 | --- DateTime: 2024/4/12 22:35 5 | --- 6 | local key = KEYS[1]; --锁的key 7 | local threadId = ARGV[1] --加上了uuid的线程唯一id 8 | 9 | --加锁的就是当前线程 10 | if (redis.call('get', key) == threadId) then 11 | -- 删除锁并返回1作为成功标志 12 | redis.call('del', key); 13 | return 1; 14 | end ; 15 | -- 在这里说明加锁的不是当前线程,解锁失败 16 | return 0; 17 | -------------------------------------------------------------------------------- /gateway/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | *.yml 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | tiktok 7 | com.zzzi 8 | 1.0 9 | ../../tiktok/pom.xml 10 | 11 | 12 | com.zzzi 13 | gateway 14 | 0.0.1-SNAPSHOT 15 | gateway 16 | 极简版抖音网关服务模块 17 | 18 | 19 | 1.8 20 | UTF-8 21 | UTF-8 22 | 2.3.9.RELEASE 23 | 24 | 25 | 26 | 27 | 28 | com.zzzi 29 | common 30 | 0.0.1-SNAPSHOT 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-data-redis 36 | 37 | 38 | com.google.code.gson 39 | gson 40 | 2.8.2 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-validation 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-starter-gateway 59 | 60 | 61 | 62 | 63 | com.alibaba.cloud 64 | spring-cloud-starter-alibaba-nacos-discovery 65 | 66 | 67 | junit 68 | junit 69 | 3.8.1 70 | test 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/zzzi/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | 7 | @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 8 | public class GatewayApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(GatewayApplication.class, args); 12 | } 13 | } 14 | 15 | /** 16 | * @author zzzi 17 | * @date 2024/3/30 13:27 18 | * todo:1. 分布式事务Seata + 多数据源管理(tiktok库读写分离,salt库读写不分离),用哪个数据源注入哪个数据源 19 | * 2. 防止缓存穿透,存储默认值,不使用布隆过滤器 20 | * todo: 3. 防止缓存雪崩:过期值打散+Sentinel限流 21 | * todo:4. 推拉模式的视频推流 22 | * 5. 用户作品缓存只缓存一部分新的,也就是leftPush之后超过长度需要rightPop 23 | * 6. 登录校验(有token说明已登录) 24 | * todo:7. 用户签到 25 | * todo:8. 改进RabbitMQ,实现异步通信的可靠性 26 | * todo:9. tiktok库一主多从复制+salt库分开,相当于动态数据源动态切换 27 | * todo:10. 本地视频获取路径 28 | * 11. 获取用户的关注列表 29 | * 12. 测试完善关注和取消关注 30 | * 13. 完善用户是否登录 31 | * 14. 测试try-catch是否吃掉回滚,也就是事务为什么失效,如何解决 32 | * 15. 注册之后直接调用接口获取用户基本信息 33 | * todo:16. 使用canal实现缓存和数据库中的数据一致性 34 | * 17. 先更新数据库再操作缓存,因为缓存速度快,尽可能减小数据不一致问题 35 | * 18. 分布式锁解锁问题,判断是不是当前线程来解锁(看原项目怎么写的) 36 | * 19. setIfAbsent的问题,用户信息应该是不过期的 37 | * 20. Redis默认值过期值为5分钟 38 | * 21. 排除所有空指针 39 | * 22. 注册成功就应该保存token 40 | * 23. is_follow代表我是否关注了当前用户,详见UserVO实体类中的解释 41 | * 24. 粉丝列表、好友列表,并进行测试,主要测试is_follow 42 | * 25. 上传功能改进,测试现有的所有功能 43 | * 26. 先将视频id缓存和视频缓存分离开 44 | * 27. 点赞/取消点赞,点赞列表 45 | * 28. 排查所有token的用途 46 | * 29. 所有QueryWrapper传递的判断条件,类型是否能匹配 47 | * 30. 交换机分离,一个交换机只干一个事 48 | * 31. 更新用户token的操作放到工具类中 49 | * 32. api请求文档 50 | * 33. 各种缓存新增前需要判断当前是否有默认值,有的话需要删除 51 | * 34. 用户登录时,推荐视频作者的关注状态 52 | * 35. 抛出自定义异常后,全局异常处理器返回的响应格式是否正确 53 | * 36. 部分请求量高的RabbitMQ消息改成Work模型,单一职责,并且能者多劳 54 | * todo: 37:看https://www.bilibili.com/video/BV11Z4y1f7cT,实现分布式事务 55 | * todo: 38:不再关注于业务,而是关注于优化 56 | * todo: 39:salt库怎么存储,因为要一个用户对应一个salt值,且需要分库存储 57 | * todo: 40:非法SQL拦截 58 | * 41:去掉代码中的循环依赖 59 | * todo: 42:视频相关表和用户相关表分开 60 | */ 61 | 62 | /* 63 | * 64 | *   ┏┓   ┏┓+ + 65 | *  ┏┛┻━━━┛┻┓ + + 66 | *  ┃       ┃ 67 | *  ┃   ━   ┃ ++ + + + 68 | * ████━████ ┃+ 69 | *  ┃       ┃ + 70 | *  ┃   ┻   ┃ 71 | *  ┃       ┃ + + 72 | *  ┗━┓   ┏━┛ 73 | *    ┃   ┃ 74 | *    ┃   ┃ + + + + 75 | *    ┃   ┃ 76 | *    ┃   ┃ + 神兽保佑 77 | *    ┃   ┃ 代码无bug 78 | *    ┃   ┃  + 79 | *    ┃    ┗━━━┓ + + 80 | *    ┃        ┣┓ 81 | *    ┃        ┏┛ 82 | *    ┗┓┓┏━┳┓┏┛ + + + + 83 | *     ┃┫┫ ┃┫┫ 84 | *     ┗┻┛ ┗┻┛+ + + + 85 | */ 86 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/zzzi/gateway/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.gateway.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.cors.CorsConfiguration; 6 | import org.springframework.web.cors.reactive.CorsWebFilter; 7 | import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; 8 | 9 | /**@author zzzi 10 | * @date 2024/3/29 14:06 11 | * 配置跨域,这种是全局跨域,只需要返回一个新的CorsWebFilter的bean即可 12 | */ 13 | @Configuration 14 | public class CorsConfig { 15 | /** 16 | * 跨域解决办法之一: 17 | * 过滤器,给所有请求增加请求头信息 18 | * 使得预检请求通过 19 | */ 20 | @Bean 21 | public CorsWebFilter corsWebFilter() { 22 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 23 | CorsConfiguration corsConfiguration = new CorsConfiguration(); 24 | // 1、配置跨域 25 | corsConfiguration.addAllowedHeader("*"); 26 | corsConfiguration.addAllowedMethod("*"); 27 | corsConfiguration.addAllowedOrigin("*"); 28 | corsConfiguration.setAllowCredentials(true);// 否则跨域请求会丢失cookie信息 29 | 30 | source.registerCorsConfiguration("/**", corsConfiguration); 31 | return new CorsWebFilter(source); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/zzzi/gateway/config/UnExpectedExitHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.gateway.config; 2 | 3 | import com.zzzi.common.constant.RedisKeys; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.DisposableBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Set; 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/4/4 17:27 15 | * 程序意外退出执行这个方法 16 | */ 17 | @Component 18 | @Slf4j 19 | public class UnExpectedExitHandler implements DisposableBean { 20 | @Autowired 21 | private StringRedisTemplate redisTemplate; 22 | 23 | /** 24 | * @author zzzi 25 | * @date 2024/4/4 17:22 26 | * 系统崩溃时删除所有用户token,防止token没过期下次登录不上 27 | */ 28 | @Override 29 | public void destroy() throws Exception { 30 | boolean state = false; 31 | Set keys = redisTemplate.keys("*"); 32 | for (String key : keys) { 33 | //只删除用户token 34 | if (key.startsWith(RedisKeys.USER_TOKEN_PREFIX)) 35 | redisTemplate.delete(key); 36 | state = true; 37 | } 38 | if (state) { 39 | log.info("清除缓存成功!"); 40 | } else { 41 | log.info("无缓存数据可清除!"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/zzzi/gateway/filter/GatewayGlobalFilter.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.gateway.filter; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 7 | import org.springframework.cloud.gateway.filter.GlobalFilter; 8 | import org.springframework.core.Ordered; 9 | import org.springframework.http.server.reactive.ServerHttpRequest; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.server.ServerWebExchange; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Component 15 | @Slf4j 16 | public class GatewayGlobalFilter implements GlobalFilter, Ordered { 17 | 18 | @Override 19 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 20 | ServerHttpRequest request = exchange.getRequest(); 21 | String path = request.getURI().getPath(); 22 | log.info("请求path:{}", path); 23 | //打印请求路径之后直接放行 24 | return chain.filter(exchange); 25 | } 26 | 27 | public int getOrder() { 28 | return 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application.txt: -------------------------------------------------------------------------------- 1 | server: 2 | port: 10010 3 | logging: 4 | level: 5 | com.zzzi: debug 6 | pattern: 7 | dateformat: MM-dd HH:mm:ss:SSS 8 | spring: 9 | application: 10 | name: gateway 11 | servlet: 12 | multipart: 13 | # 单词请求和单词文件的最大值 14 | max-request-size: 1024MB 15 | max-file-size: 1024MB 16 | cloud: 17 | # nacos相关地址 18 | nacos: 19 | server-addr: localhost:8848 20 | # sentinel相关配置 21 | sentinel: 22 | transport: 23 | dashboard: localhost:8080 24 | # 网关相关配置 25 | gateway: 26 | discovery: 27 | locator: 28 | # 开启从注册中心动态创建路由的功能,利用微服务名进行路由 29 | enabled: true 30 | routes: 31 | - id: userservice # 路由标示,必须唯一 32 | uri: lb://userservice # 路由的目标地址 33 | predicates: # 路由断言,判断请求是否符合规则 34 | - Path=/douyin/user/**,/douyin/relation/**,/douyin/message/** 35 | - id: videoservice 36 | uri: lb://videoservice 37 | predicates: 38 | - Path=/douyin/feed/**,/douyin/publish/**,/douyin/favorite/**,/douyin/comment/** 39 | -------------------------------------------------------------------------------- /gateway/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.GREEN} 2 | ██████╗ █████╗ ████████╗███████╗██╗ ██╗ █████╗ ██╗ ██╗ 3 | ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗╚██╗ ██╔╝ 4 | ██║ ███╗███████║ ██║ █████╗ ██║ █╗ ██║███████║ ╚████╔╝ 5 | ██║ ██║██╔══██║ ██║ ██╔══╝ ██║███╗██║██╔══██║ ╚██╔╝ 6 | ╚██████╔╝██║ ██║ ██║ ███████╗╚███╔███╔╝██║ ██║ ██║ 7 | ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ 8 | ${AnsiColor.BRIGHT_BLACK} -------------------------------------------------------------------------------- /gateway/src/test/java/com/zzzi/gateway/GatewayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.gateway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class GatewayApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.zzzi 8 | tiktok 9 | 1.0 10 | 11 | 12 | 13 | ./common 14 | ./video-service 15 | ./user-service 16 | ./gateway 17 | 18 | 19 | pom 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-parent 24 | 2.3.9.RELEASE 25 | 26 | 27 | 28 | 29 | UTF-8 30 | UTF-8 31 | 1.8 32 | Hoxton.RELEASE 33 | 5.1.46 34 | 3.5.1 35 | 0.7.0 36 | 2.3.9.RELEASE 37 | 2.3.1.RELEASE 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-amqp 48 | ${springboot.amqp} 49 | 50 | 51 | io.jsonwebtoken 52 | jjwt 53 | ${jwt.version} 54 | 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-dependencies 59 | ${spring-cloud.version} 60 | pom 61 | import 62 | 63 | 64 | 65 | com.alibaba.cloud 66 | spring-cloud-alibaba-dependencies 67 | 2.2.0.RELEASE 68 | pom 69 | import 70 | 71 | 72 | 73 | mysql 74 | mysql-connector-java 75 | ${mysql.version} 76 | 77 | 78 | 79 | com.baomidou 80 | mybatis-plus-boot-starter 81 | ${mybatisplus.version} 82 | 83 | 84 | 85 | 86 | 87 | org.projectlombok 88 | lombok 89 | 90 | 91 | 92 | org.apache.commons 93 | commons-dbcp2 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-starter-jdbc 99 | 100 | 101 | 102 | com.zaxxer 103 | HikariCP 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /resource/RabbitMQ设计.md: -------------------------------------------------------------------------------- 1 | ### RabbitMQ设计 2 | 3 | > 在这里设计RabbitMQ需要接收哪些消息,交换机和队列的名称是什么 4 | 5 | ![RabbitMQ](./img/RabbitMQ.jpg) 6 | 7 | ### Direct模式 8 | 9 | 这个模式涉及到交换机,一个交换机下有多个队列,每个队列都有自己的事情,例如点赞和取消点赞都在FAVORITE_EXCHANGE交换机下,但是消费的消息是相反的 10 | 11 | 尽量做到了单一职责,每个队列只负责单一的事情 12 | 13 | ### WorkQueue模式 14 | 15 | 这个模式下不涉及到交换机,只有四个工作模式的队列,每个队列下有两个消费者,目的是为了提高异步消息处理的速度 16 | 17 | ### RabbitMQKeys 18 | 19 | RabbitMQ涉及到的常量如下: 20 | 21 | ```java 22 | package com.zzzi.common.constant; 23 | 24 | /** 25 | * @author zzzi 26 | * @date 2024/3/25 18:46 27 | * RabbitMQ的相关设计 28 | */ 29 | public class RabbitMQKeys { 30 | 31 | /** 32 | * MQ中交换机名称 33 | */ 34 | public static final String POST_VIDEO_EXCHANGE = "tiktok.post_video"; 35 | public static final String FOLLOW_EXCHANGE = "tiktok.follow"; 36 | public static final String COMMENT_EXCHANGE = "tiktok.comment"; 37 | public static final String ERROR_EXCHANGE = "error.direct"; 38 | 39 | 40 | /** 41 | * MQ中关于消费失败的key 42 | */ 43 | public static final String ERROR = "error"; 44 | 45 | /** 46 | * MQ中关于点赞的queue 47 | * 点赞和取消点赞需要分开 48 | * 并且点赞针对用户和视频的key也需要分开 49 | */ 50 | public static final String FAVORITE_USER = "work.favorite_user"; 51 | public static final String UN_FAVORITE_USER = "work.un_favorite_user"; 52 | public static final String FAVORITE_VIDEO = "work.favorite_video"; 53 | public static final String UN_FAVORITE_VIDEO = "work.un_favorite_video"; 54 | /** 55 | * MQ中关于评论的key 56 | */ 57 | public static final String COMMENT_KEY = "comment"; 58 | public static final String UN_COMMENT_KEY = "un_comment"; 59 | /** 60 | * MQ中关于关注的key 61 | * 关注和取消关注需要分开 62 | */ 63 | public static final String FOLLOW_KEY = "follow"; 64 | public static final String UN_FOLLOW_KEY = "un_follow"; 65 | 66 | /** 67 | * MQ中关于投稿的key 68 | */ 69 | public static final String VIDEO_POST = "video_post"; 70 | } 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /resource/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzziCode/tiktok/2bac0bd7cc81a49609134494b0f81212077be9ca/resource/app-release.apk -------------------------------------------------------------------------------- /resource/img/RabbitMQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzziCode/tiktok/2bac0bd7cc81a49609134494b0f81212077be9ca/resource/img/RabbitMQ.jpg -------------------------------------------------------------------------------- /resource/img/framework.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzziCode/tiktok/2bac0bd7cc81a49609134494b0f81212077be9ca/resource/img/framework.jpg -------------------------------------------------------------------------------- /user-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | *.yml 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/UserServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice; 2 | 3 | import com.alibaba.cloud.nacos.ribbon.NacosRule; 4 | import com.netflix.loadbalancer.IRule; 5 | import com.zzzi.common.constant.RedisKeys; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.transaction.annotation.EnableTransactionManagement; 14 | 15 | import javax.annotation.PreDestroy; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | 19 | @SpringBootApplication(scanBasePackages = {"com.zzzi.*"}) 20 | //开启事务管理 21 | @EnableTransactionManagement 22 | @EnableAspectJAutoProxy(exposeProxy = true) 23 | public class UserServiceApplication { 24 | 25 | public static void main(String[] args) { 26 | SpringApplication.run(UserServiceApplication.class, args); 27 | } 28 | 29 | //负载均衡规则 30 | @Bean 31 | public IRule rule() { 32 | return new NacosRule(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/MyMetaObjectHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import com.zzzi.common.exception.UserException; 5 | import com.zzzi.common.exception.VideoException; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.ibatis.reflection.MetaObject; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Date; 11 | 12 | @Component 13 | @Slf4j 14 | /**@author zzzi 15 | * @date 2024/3/27 19:25 16 | * 在这里进行基本的属性回显 17 | * 更加复杂的属性回显后期操作 18 | */ 19 | public class MyMetaObjectHandler implements MetaObjectHandler { 20 | 21 | @Override 22 | public void insertFill(MetaObject metaObject) { 23 | try { 24 | //todo 实体类的id使用雪花算法生成全局唯一ID 25 | this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); 26 | //更新时间不管什么时候都自动更新 27 | this.strictInsertFill(metaObject, "updateTime", Date.class, new Date()); 28 | this.strictInsertFill(metaObject, "followCount", Integer.class, 0); 29 | this.strictInsertFill(metaObject, "followerCount", Integer.class, 0); 30 | this.strictInsertFill(metaObject, "avatar", String.class, "https://zzzi-img-1313100942.cos.ap-beijing.myqcloud.com/tiktok_avatar.jpg"); 31 | this.strictInsertFill(metaObject, "backgroundImage", String.class, "https://zzzi-img-1313100942.cos.ap-beijing.myqcloud.com/background_image.jpg"); 32 | this.strictInsertFill(metaObject, "signature", String.class, "谢谢你的关注"); 33 | this.strictInsertFill(metaObject, "totalFavorited", Long.class, 0L); 34 | this.strictInsertFill(metaObject, "workCount", Integer.class, 0); 35 | this.strictInsertFill(metaObject, "favoriteCount", Integer.class, 0); 36 | } catch (Exception e) { 37 | log.error(e.getMessage()); 38 | throw new UserException("属性自动填充失败"); 39 | } 40 | } 41 | 42 | @Override 43 | public void updateFill(MetaObject metaObject) { 44 | try { 45 | //更新时间不管如何都直接填充 46 | this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); 47 | } catch (Exception e) { 48 | log.error(e.getMessage()); 49 | throw new UserException("属性自动填充失败"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/MybatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * @author zzzi 11 | * @date 2024/4/2 17:07 12 | * 在这里配置分页插件 13 | */ 14 | @Configuration 15 | public class MybatisPlusConfig { 16 | 17 | /** 18 | * 添加分页插件 19 | */ 20 | @Bean 21 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 22 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 23 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加 24 | return interceptor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/RabbitMQConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import com.zzzi.common.constant.RabbitMQKeys; 4 | import org.springframework.amqp.core.Binding; 5 | import org.springframework.amqp.core.BindingBuilder; 6 | import org.springframework.amqp.core.DirectExchange; 7 | import org.springframework.amqp.core.Queue; 8 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 9 | import org.springframework.amqp.rabbit.retry.MessageRecoverer; 10 | import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; 11 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 12 | import org.springframework.amqp.support.converter.MessageConverter; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | 17 | /** 18 | * @author zzzi 19 | * @date 2024/3/27 16:24 20 | * 这可以使得RabbitMQ使用json序列化,而不是使用java中的jdk序列化 21 | */ 22 | @Configuration 23 | public class RabbitMQConfig { 24 | @Bean 25 | public MessageConverter jsonMessageConverter() { 26 | return new Jackson2JsonMessageConverter(); 27 | } 28 | 29 | /** 30 | * @author zzzi 31 | * @date 2024/4/4 19:07 32 | * 定义两个Work模式的队列,监听点赞消息,用来更新用户信息 33 | */ 34 | @Bean 35 | public Queue createFavoriteUserQueue() { 36 | return new Queue(RabbitMQKeys.FAVORITE_USER); 37 | } 38 | 39 | @Bean 40 | public Queue createUnFavoriteUserQueue() { 41 | return new Queue(RabbitMQKeys.UN_FAVORITE_USER); 42 | } 43 | 44 | /** 45 | * @author zzzi 46 | * @date 2024/4/9 16:01 47 | * 消费者消费消息失败时会进行重试 48 | * 重试次数达到设置的上限时会将失败的消息投放到这个队列中 49 | */ 50 | 51 | //定义错误消息的交换机 52 | @Bean 53 | public DirectExchange errorMessageExchange() { 54 | return new DirectExchange(RabbitMQKeys.ERROR_EXCHANGE); 55 | } 56 | 57 | //定义错误消息的队列 58 | @Bean 59 | public Queue errorQueue() { 60 | return new Queue("error.queue", true); 61 | } 62 | 63 | //定义二者之间的绑定关系 64 | @Bean 65 | public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange) { 66 | return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with(RabbitMQKeys.ERROR); 67 | } 68 | 69 | @Bean 70 | public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) { 71 | //重试次数耗尽时,会将失败的消息发送给指定的队列 72 | return new RepublishMessageRecoverer(rabbitTemplate, RabbitMQKeys.ERROR_EXCHANGE, RabbitMQKeys.ERROR); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/ReturnCallBack.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.context.ApplicationContextAware; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | 11 | @Configuration 12 | @Slf4j 13 | /**@author zzzi 14 | * @date 2024/4/9 15:24 15 | * 定义消息发送失败的回调 16 | * 消息到达交换机但是没有到达队列时触发 17 | */ 18 | public class ReturnCallBack implements ApplicationContextAware { 19 | @Override 20 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 21 | // 获取RabbitTemplate 22 | RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); 23 | // 设置ReturnCallback 24 | rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { 25 | // 投递失败,记录日志 26 | log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}", 27 | replyCode, replyText, exchange, routingKey, message.toString()); 28 | // 如果有业务需要,可以重发消息 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/UnExpectedExitHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import com.zzzi.common.constant.RedisKeys; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.DisposableBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Set; 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/4/4 17:27 15 | * 程序意外退出执行这个方法 16 | */ 17 | @Component 18 | @Slf4j 19 | public class UnExpectedExitHandler implements DisposableBean { 20 | @Autowired 21 | private StringRedisTemplate redisTemplate; 22 | 23 | /** 24 | * @author zzzi 25 | * @date 2024/4/4 17:22 26 | * 系统崩溃时删除所有用户token,防止token没过期下次登录不上 27 | */ 28 | @Override 29 | public void destroy() { 30 | boolean state = false; 31 | Set keys = redisTemplate.keys("*"); 32 | for (String key : keys) { 33 | //只删除用户token 34 | if (key.startsWith(RedisKeys.USER_TOKEN_PREFIX)) 35 | redisTemplate.delete(key); 36 | state = true; 37 | } 38 | if (state) { 39 | log.info("清除缓存成功!"); 40 | } else { 41 | log.info("无缓存数据可清除!"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.config; 2 | 3 | import com.zzzi.userservice.interceptor.LoginUserInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.core.StringRedisTemplate; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | import javax.annotation.Resource; 10 | 11 | /** 12 | * @author zzzi 13 | * @date 2024/3/29 15:23 14 | * 给当前项目配置拦截器 15 | */ 16 | @Configuration 17 | public class WebConfig implements WebMvcConfigurer { 18 | 19 | @Override 20 | public void addInterceptors(InterceptorRegistry registry) { 21 | // 只拦截需要登录校验的请求.比如登录/评论/点赞/发布视频/用户关注/推送视频 22 | // 除了登录和获取资源还有推送视频不拦截,其他的都拦截 23 | registry.addInterceptor(new LoginUserInterceptor()).excludePathPatterns( 24 | "/douyin/user/**", 25 | "/resource/**", 26 | "/douyin/feed/**" 27 | ).order(1); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.controller; 2 | 3 | import com.zzzi.common.result.CommonVO; 4 | import com.zzzi.common.result.MessageListVO; 5 | import com.zzzi.common.result.MessageVO; 6 | import com.zzzi.userservice.service.MessageService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.List; 15 | 16 | @Slf4j 17 | @RestController 18 | @RequestMapping("/douyin/message") 19 | public class MessageController { 20 | 21 | @Autowired 22 | private MessageService messageService; 23 | 24 | /** 25 | * @author zzzi 26 | * @date 2024/4/3 16:02 27 | * 用户发送消息 28 | * 目前只实现发送消息,不实现撤回消息 29 | */ 30 | @PostMapping("/action") 31 | public CommonVO messageAction(String token, String to_user_id, String action_type, String content) { 32 | log.info("用户发送消息,token为:{},to_user_id为:{}", token, to_user_id); 33 | String status_msg = ""; 34 | //截取真正的token 35 | if (token.startsWith("login:token:")) 36 | token = token.substring(12); 37 | if ("1".equals(action_type)) { 38 | messageService.messageAction(token, to_user_id, content); 39 | status_msg = "成功发送消息"; 40 | } else { 41 | //todo: 撤回消息 42 | } 43 | return CommonVO.success(status_msg); 44 | } 45 | 46 | 47 | /** 48 | * @author zzzi 49 | * @date 2024/4/3 16:04 50 | * 获取好友之间的聊天记录 51 | */ 52 | @GetMapping("/chat") 53 | public MessageListVO getMessageList(String token, String to_user_id) { 54 | log.info("获取好友之间的聊天记录,token为:{},to_user_id为:{}", token, to_user_id); 55 | //截取真正的token 56 | if (token.startsWith("login:token:")) 57 | token = token.substring(12); 58 | List message_list = messageService.getMessageList(token, to_user_id); 59 | if (message_list != null) { 60 | return MessageListVO.success("成功获取聊天记录", message_list); 61 | } 62 | return MessageListVO.fail("获取聊天记录失败"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/controller/RelationController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.controller; 2 | 3 | import com.zzzi.common.result.CommonVO; 4 | import com.zzzi.common.result.UserVO; 5 | import com.zzzi.common.result.UserRelationListVO; 6 | import com.zzzi.userservice.service.RelationService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.List; 15 | 16 | @RestController 17 | @RequestMapping("/douyin/relation") 18 | @Slf4j 19 | public class RelationController { 20 | @Autowired 21 | private RelationService relationService; 22 | 23 | /** 24 | * @author zzzi 25 | * @date 2024/3/29 16:17 26 | * 根据用户传递来的操作判断是关注还是取消关注 27 | */ 28 | @PostMapping("/action") 29 | public CommonVO followAction(String token, Long to_user_id, String action_type) { 30 | log.info("用户关注操作,,token为:{},to_user_id为:{}", token, to_user_id); 31 | String status_msg = ""; 32 | //截取真正的token,去掉前缀"login:token:" 33 | if (token.startsWith("login:token:")) 34 | token = token.substring(12); 35 | if ("1".equals(action_type)) { 36 | relationService.followAction(token, to_user_id); 37 | status_msg = "成功关注"; 38 | } else { 39 | relationService.followUnAction(token, to_user_id); 40 | status_msg = "成功取消关注"; 41 | } 42 | return CommonVO.success(status_msg); 43 | } 44 | 45 | /** 46 | * @author zzzi 47 | * @date 2024/3/29 22:12 48 | * 获取当前用户的所有关注列表 49 | */ 50 | @GetMapping("/follow/list") 51 | public UserRelationListVO getFollowList(String user_id, String token) { 52 | log.info("获取用户关注列表,token为:{},user_id为:{}", token, user_id); 53 | //截取真正的token,去掉前缀"login:token:" 54 | if (token.startsWith("login:token:")) 55 | token = token.substring(12); 56 | List user_list = relationService.getFollowList(user_id, token); 57 | if (user_list == null || user_list.isEmpty()) { 58 | return UserRelationListVO.fail("用户关注列表为空"); 59 | } 60 | return UserRelationListVO.success("获取关注列表成功", user_list); 61 | } 62 | 63 | /** 64 | * @author zzzi 65 | * @date 2024/4/1 12:40 66 | * 获取当前用户的所有粉丝列表 67 | */ 68 | @GetMapping("/follower/list") 69 | public UserRelationListVO getFollowerList(String user_id, String token) { 70 | log.info("获取用户粉丝列表,token为:{},user_id为:{}", token, user_id); 71 | //截取真正的token,去掉前缀"login:token:" 72 | if (token.startsWith("login:token:")) 73 | token = token.substring(12); 74 | List user_list = relationService.getFollowerList(user_id, token); 75 | if (user_list == null || user_list.isEmpty()) { 76 | return UserRelationListVO.fail("用户粉丝列表为空"); 77 | } 78 | return UserRelationListVO.success("获取粉丝列表成功", user_list); 79 | } 80 | 81 | /** 82 | * @author zzzi 83 | * @date 2024/4/1 12:40 84 | * 获取当前用户所有的好友列表 85 | */ 86 | @GetMapping("/friend/list") 87 | public UserRelationListVO getFriendList(String user_id, String token) { 88 | log.info("获取用户好友列表,token为:{},user_id为:{}", token, user_id); 89 | //截取真正的token,去掉前缀"login:token:" 90 | if (token.startsWith("login:token:")) 91 | token = token.substring(12); 92 | List user_list = relationService.getFriendList(user_id, token); 93 | if (user_list == null || user_list.isEmpty()) { 94 | return UserRelationListVO.fail("用户好友列表为空"); 95 | } 96 | return UserRelationListVO.success("获取好友列表成功", user_list); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.controller; 2 | 3 | 4 | import com.zzzi.common.result.UserInfoVO; 5 | import com.zzzi.userservice.dto.UserDTO; 6 | import com.zzzi.common.result.UserRegisterLoginVO; 7 | import com.zzzi.common.result.UserVO; 8 | import com.zzzi.userservice.service.UserService; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | 14 | @RestController 15 | @RequestMapping("/douyin/user") 16 | @Slf4j 17 | public class UserController { 18 | 19 | @Autowired 20 | private UserService userService; 21 | 22 | /** 23 | * @author zzzi 24 | * @date 2024/3/27 10:38 25 | * 用户注册,会根据用户id和用户姓名创建一个token返回 26 | */ 27 | @PostMapping("/register") 28 | public UserRegisterLoginVO register(String username, String password) { 29 | log.info("注册时的用户名为:{},用户密码为:{}", username, password); 30 | UserDTO userDTO = userService.register(username, password); 31 | 32 | //封装需要的数据返回 33 | return UserRegisterLoginVO.success(userDTO.getUserDO().getUserId(), userDTO.getToken()); 34 | } 35 | 36 | /** 37 | * @author zzzi 38 | * @date 2024/3/27 10:39 39 | * 用户登录,也会根据用户id和用户姓名创建一个token返回 40 | * 由于签名算法和签名秘钥一致,所以注册登录的token一致 41 | */ 42 | @PostMapping("/login") 43 | public UserRegisterLoginVO login(String username, String password) { 44 | log.info("登录时的用户名为:{},用户密码为:{}", username, password); 45 | UserDTO userDTO = userService.login(username, password); 46 | 47 | //封装需要的数据返回 48 | return UserRegisterLoginVO.success(userDTO.getUserDO().getUserId(), userDTO.getToken()); 49 | } 50 | 51 | /** 52 | * @author zzzi 53 | * @date 2024/3/27 10:39 54 | * 获取当前登录用户全部信息 55 | * 这个也可以用来做远程调用 56 | */ 57 | @GetMapping 58 | public UserInfoVO userInfo(String user_id, @RequestParam(required = false) String token) { 59 | log.info("获取用户信息的用户id为:{},用户token为:{}", user_id, token); 60 | if (token != null&&token.startsWith("login:token:")) { 61 | token = token.substring(12); 62 | } 63 | log.info("截取之后的用户token为:{}", token); 64 | UserVO user = userService.getUserInfo(user_id); 65 | if (user == null) 66 | return UserInfoVO.fail("当前用户不存在,请先注册"); 67 | //将后端封装好的userVO返回给前端 68 | return UserInfoVO.success(user); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.dto; 2 | 3 | 4 | import com.zzzi.userservice.entity.UserDO; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | /**@author zzzi 10 | * @date 2024/3/26 21:27 11 | * 三层之间传递使用这个数据对象 12 | */ 13 | //@Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class UserDTO { 17 | private UserDO userDO; 18 | private String token; 19 | 20 | public String getToken() { 21 | return token; 22 | } 23 | 24 | public void setToken(String token) { 25 | this.token = token; 26 | } 27 | 28 | public UserDO getUserDO() { 29 | return userDO; 30 | } 31 | 32 | public void setUserDO(UserDO userDO) { 33 | this.userDO = userDO; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/entity/MessageDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Date; 13 | 14 | /** 15 | * @author zzzi 16 | * @date 2024/4/3 16:12 17 | * 消息对应的数据库实体类 18 | */ 19 | @TableName("message") 20 | @Data 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | public class MessageDO extends Model { 24 | @TableId 25 | private Long messageId; 26 | private Long fromUserId; 27 | private Long toUserId; 28 | private String content; 29 | @TableField(fill = FieldFill.INSERT) 30 | private Date createTime; 31 | } 32 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/entity/UserDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Date; 13 | 14 | /** 15 | * @author zzzi 16 | * @date 2024/3/26 21:17 17 | * User的数据库实体类 18 | */ 19 | @TableName("users") 20 | @Data 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | public class UserDO extends Model { 24 | 25 | @TableId 26 | private Long userId; 27 | private String username; 28 | private String email; 29 | private String password; 30 | @TableField(fill = FieldFill.INSERT) 31 | private Date createTime; 32 | @TableField(fill = FieldFill.INSERT_UPDATE) 33 | private Date updateTime; 34 | @TableField(fill = FieldFill.INSERT) 35 | private Integer followCount; 36 | @TableField(fill = FieldFill.INSERT) 37 | private Integer followerCount; 38 | @TableField(fill = FieldFill.INSERT) 39 | private String avatar; 40 | @TableField(fill = FieldFill.INSERT) 41 | private String backgroundImage; 42 | @TableField(fill = FieldFill.INSERT) 43 | private String signature; 44 | @TableField(fill = FieldFill.INSERT) 45 | private Long totalFavorited; 46 | @TableField(fill = FieldFill.INSERT) 47 | private Integer workCount; 48 | @TableField(fill = FieldFill.INSERT) 49 | private Integer favoriteCount; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/entity/UserFollowDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableId; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.baomidou.mybatisplus.extension.activerecord.Model; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * @author zzzi 12 | * @date 2024/3/29 16:15 13 | * 用户关注实体 14 | */ 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @TableName("user_follows") 19 | public class UserFollowDO extends Model { 20 | @TableId 21 | private Long followId; 22 | //谁点了关注 23 | private Long followerId; 24 | //被关注者是谁 25 | private Long followedId; 26 | } 27 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/interceptor/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.interceptor; 2 | 3 | import com.zzzi.common.exception.FollowException; 4 | import com.zzzi.common.exception.RelationException; 5 | import com.zzzi.common.exception.UserException; 6 | import com.zzzi.common.exception.UserInfoException; 7 | import com.zzzi.common.result.CommonVO; 8 | import com.zzzi.common.result.UserInfoVO; 9 | import com.zzzi.common.result.UserRegisterLoginVO; 10 | import com.zzzi.common.result.UserRelationListVO; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.security.auth.login.LoginException; 17 | 18 | 19 | /** 20 | * @author zzzi 21 | * @date 2024/3/26 22:34 22 | * 在这里处理userservice中的所有异常 23 | */ 24 | @ControllerAdvice(annotations = {RestController.class}) 25 | @ResponseBody 26 | @Slf4j 27 | public class GlobalExceptionHandler { 28 | 29 | @ExceptionHandler(UserException.class) 30 | public UserRegisterLoginVO RegisterExceptionHandler(UserException ex) { 31 | log.error(ex.getMessage()); 32 | 33 | if (ex.getMessage().contains("邮箱格式不正确")) { 34 | return UserRegisterLoginVO.fail("邮箱格式不正确"); 35 | } 36 | if (ex.getMessage().contains("用户名被占用,请重新输入用户名")) { 37 | return UserRegisterLoginVO.fail("用户名被占用,请重新输入用户名"); 38 | } 39 | if (ex.getMessage().contains("登录失败,请确认用户是否注册或者用户名和密码是否正确")) { 40 | return UserRegisterLoginVO.fail("登录失败,请确认用户是否注册或者用户名和密码是否正确"); 41 | } 42 | if (ex.getMessage().contains("用户未登录,请先去登录")) { 43 | return UserRegisterLoginVO.fail("用户未登录,请先去登录"); 44 | } 45 | if (ex.getMessage().contains("更新用户信息失败")) { 46 | return UserRegisterLoginVO.fail("更新用户信息失败"); 47 | } 48 | if (ex.getMessage().contains("当前用户已经登录,请不要重复登录")) { 49 | return UserRegisterLoginVO.fail("当前用户已经登录,请不要重复登录"); 50 | } 51 | return UserRegisterLoginVO.fail("未知错误"); 52 | } 53 | 54 | @ExceptionHandler(LoginException.class) 55 | public CommonVO LoginExceptionHandler(LoginException ex) { 56 | log.error(ex.getMessage()); 57 | return CommonVO.fail("请先登录"); 58 | } 59 | 60 | @ExceptionHandler(RuntimeException.class) 61 | public CommonVO CommonExceptionHandler(RuntimeException ex) { 62 | log.error(ex.getMessage()); 63 | if (ex.getMessage().contains("用户点赞失败")) { 64 | return CommonVO.fail("用户点赞失败"); 65 | } 66 | if (ex.getMessage().contains("请勿重复点赞")) { 67 | return CommonVO.fail("请勿重复点赞"); 68 | } 69 | if (ex.getMessage().contains("取消点赞失败")) { 70 | return CommonVO.fail("取消点赞失败"); 71 | } 72 | if (ex.getMessage().contains("用户发送消息失败")) { 73 | return CommonVO.fail("用户发送消息失败"); 74 | } 75 | if (ex.getMessage().contains("用户投稿失败")) { 76 | return CommonVO.fail("用户投稿失败"); 77 | } 78 | return CommonVO.fail("出现错误"); 79 | } 80 | 81 | @ExceptionHandler(RelationException.class) 82 | public UserRelationListVO RelationExceptionHandler(RelationException ex) { 83 | log.error(ex.getMessage()); 84 | 85 | if (ex.getMessage().contains("由于用户隐私设置,获取用户关注列表失败")) { 86 | return UserRelationListVO.fail("由于用户隐私设置,获取用户关注列表失败"); 87 | } 88 | if (ex.getMessage().contains("由于用户隐私设置,获取用户粉丝列表失败")) { 89 | return UserRelationListVO.fail("由于用户隐私设置,获取用户粉丝列表失败"); 90 | } 91 | if (ex.getMessage().contains("获取用户关注列表失败")) { 92 | return UserRelationListVO.fail("获取用户关注列表失败"); 93 | } 94 | if (ex.getMessage().contains("获取用户粉丝列表失败")) { 95 | return UserRelationListVO.fail("获取用户粉丝列表失败"); 96 | } 97 | if (ex.getMessage().contains("获取用户好友列表失败")) { 98 | return UserRelationListVO.fail("获取用户好友列表失败"); 99 | } 100 | return UserRelationListVO.fail("出现错误"); 101 | } 102 | 103 | @ExceptionHandler(UserInfoException.class) 104 | public UserInfoVO UserInfoExceptionHandler(UserInfoException ex) { 105 | log.error(ex.getMessage()); 106 | if (ex.getMessage().contains("获取用户信息失败")) { 107 | return UserInfoVO.fail("获取用户信息失败"); 108 | } 109 | 110 | return UserInfoVO.fail("未知错误"); 111 | } 112 | 113 | @ExceptionHandler(FollowException.class) 114 | public CommonVO FollowExceptionHandler(FollowException ex) { 115 | log.error(ex.getMessage()); 116 | if (ex.getMessage().contains("关注失败,不能重复关注")) { 117 | return CommonVO.fail("关注失败,不能重复关注"); 118 | } 119 | if (ex.getMessage().contains("取消关注失败")) { 120 | return CommonVO.fail("取消关注失败"); 121 | } 122 | if (ex.getMessage().contains("自己不能取消关注自己")) { 123 | return CommonVO.fail("自己不能取消关注自己"); 124 | } 125 | if (ex.getMessage().contains("自己不能关注自己")) { 126 | return CommonVO.fail("自己不能关注自己"); 127 | } 128 | if (ex.getMessage().contains("用户关注失败")) { 129 | return CommonVO.fail("用户关注失败"); 130 | } 131 | return CommonVO.fail("未知错误"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/interceptor/LoginUserInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.interceptor; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zzzi.common.constant.RedisKeys; 5 | import com.zzzi.common.result.CommonVO; 6 | import com.zzzi.common.utils.JwtUtils; 7 | import com.zzzi.common.utils.UpdateTokenUtils; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.AntPathMatcher; 15 | import org.springframework.web.servlet.HandlerInterceptor; 16 | 17 | import javax.security.auth.login.LoginException; 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.servlet.http.HttpServletResponse; 20 | 21 | /** 22 | * @author zzzi 23 | * @date 2024/3/29 14:52 24 | * 不需要拦截的请求直接放行 25 | */ 26 | @Component 27 | @Slf4j 28 | public class LoginUserInterceptor implements HandlerInterceptor { 29 | @Autowired 30 | private UpdateTokenUtils updateTokenUtils; 31 | 32 | //除了登录注册,其余的请求都需要带上token,没带就拦截 33 | @Override 34 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 35 | // 放行无需登录的请求 36 | String uri = request.getRequestURI(); 37 | AntPathMatcher antPathMatcher = new AntPathMatcher();// 匹配器 38 | boolean register = antPathMatcher.match("/douyin/user/register/**", uri);// 注册 39 | boolean login = antPathMatcher.match("/douyin/user/login/**", uri);// 登录 40 | log.info("拦截请求:" + uri); 41 | //放行无需登录的请求 42 | if (register || login) { 43 | log.info("注册或登录请求无需拦截"); 44 | return true; 45 | } 46 | 47 | // 验证登录状态 48 | /**@author zzzi 49 | * @date 2024/3/29 14:53 50 | * 直接根据缓存中是否存在该用户的token来判断 51 | */ 52 | //String token = request.getParameter("token"); 53 | //log.info("拦截到的请求中,token为:{}", token); 54 | ////截取得到真正的token 55 | //if (token != null && !"".equals(token)) { 56 | // token = token.substring(12); 57 | //} 58 | ////没有抛异常的话就是验签成功 59 | //Long userId = JwtUtils.getUserIdByToken(token); 60 | //todo 登录校验好像有点问题 61 | //String userToken = redisTemplate.opsForValue().get(RedisKeys.USER_TOKEN_PREFIX + userId); 62 | //if (userToken == null || "".equals(userToken)) { 63 | // log.error("用户未登录,非法请求"); 64 | // CommonVO fail = CommonVO.fail("请先登录"); 65 | // String failString = JSONObject.toJSONString(fail); 66 | // response.getWriter().write(failString); 67 | // return false; 68 | //} 69 | //到这一步说明校验完成,此时直接更新用户的token有效期 70 | //updateTokenUtils.updateTokenExpireTimeUtils(userId.toString()); 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/FavoriteListenerOne.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.constant.RedisKeys; 7 | import com.zzzi.userservice.entity.UserDO; 8 | import com.zzzi.userservice.mapper.UserMapper; 9 | import com.zzzi.common.utils.UpdateUserInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.StringRedisTemplate; 15 | import org.springframework.messaging.handler.annotation.Payload; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | 20 | /** 21 | * @author zzzi 22 | * @date 2024/3/29 16:38 23 | * 在这里异步的更新用户的基本信息 24 | */ 25 | @Service 26 | @Slf4j 27 | public class FavoriteListenerOne { 28 | @Autowired 29 | private UserMapper userMapper; 30 | @Autowired 31 | private UpdateUserInfoUtils updateUserInfoUtils; 32 | @Autowired 33 | private Gson gson; 34 | @Autowired 35 | private StringRedisTemplate redisTemplate; 36 | 37 | 38 | /** 39 | * @author zzzi 40 | * @date 2024/3/29 16:49 41 | * 用户关注在这里更新双方的用户信息 42 | * 更新A的点赞数,B的获赞总数 43 | */ 44 | @RabbitListener(queues = {RabbitMQKeys.FAVORITE_USER}) 45 | @Transactional 46 | public void listenToFavorite(@Payload Long[] ids) { 47 | log.info("第一个消费者监听到用户点赞操作,更新用户信息"); 48 | log.info("第一个消费者监听到用户点赞操作,点赞人id为:{}", ids[0]); 49 | log.info("第一个消费者监听到用户点赞操作,更新用户信息,获赞人id为:{}", ids[1]); 50 | //两个用户都更新 51 | UserDO userA = userMapper.selectById(ids[0]); 52 | 53 | 54 | //todo 数据库更新时,尝试加上乐观锁,防止多线程出现问题 55 | //A的点赞数+1 56 | Integer favoriteCount = userA.getFavoriteCount(); 57 | LambdaQueryWrapper queryWrapperA = new LambdaQueryWrapper<>(); 58 | //判断更新时别人是否更新过了 59 | queryWrapperA.eq(UserDO::getFavoriteCount, favoriteCount); 60 | userA.setFavoriteCount(favoriteCount + 1); 61 | int updateA = userMapper.update(userA, queryWrapperA); 62 | if (updateA != 1) { 63 | //更新失败需要重试,手动实现CAS算法 64 | FavoriteListenerOne favoriteListener = (FavoriteListenerOne) AopContext.currentProxy(); 65 | favoriteListener.listenToFavorite(ids); 66 | } 67 | //B的获赞总数+1 68 | UserDO userB = userMapper.selectById(ids[1]); 69 | Long totalFavorited = userB.getTotalFavorited(); 70 | LambdaQueryWrapper queryWrapperB = new LambdaQueryWrapper<>(); 71 | //加上乐观锁 72 | queryWrapperB.eq(UserDO::getTotalFavorited, totalFavorited); 73 | userB.setTotalFavorited(totalFavorited + 1); 74 | 75 | /**@author zzzi 76 | * @date 2024/4/14 17:11 77 | * 当前用户的获赞总数超过1W就将其保存到缓存中,认为是大V 78 | */ 79 | if (totalFavorited + 1 >= 10000) 80 | redisTemplate.opsForSet().add(RedisKeys.USER_HOT, ids[1].toString()); 81 | int updateB = userMapper.update(userB, queryWrapperB); 82 | if (updateB != 1) { 83 | //更新失败需要重试,手动实现CAS算法 84 | FavoriteListenerOne favoriteListener = (FavoriteListenerOne) AopContext.currentProxy(); 85 | favoriteListener.listenToFavorite(ids); 86 | } 87 | 88 | //更新两个用户的缓存信息 89 | 90 | String userAJson = gson.toJson(userA); 91 | String userBJson = gson.toJson(userB); 92 | 93 | updateUserInfoUtils.updateUserInfoCache(userA.getUserId(), userAJson); 94 | updateUserInfoUtils.updateUserInfoCache(userB.getUserId(), userBJson); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/FavoriteListenerTwo.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.constant.RedisKeys; 7 | import com.zzzi.userservice.entity.UserDO; 8 | import com.zzzi.userservice.mapper.UserMapper; 9 | import com.zzzi.common.utils.UpdateUserInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.StringRedisTemplate; 15 | import org.springframework.messaging.handler.annotation.Payload; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | 20 | /** 21 | * @author zzzi 22 | * @date 2024/3/29 16:38 23 | * 在这里异步的更新用户的基本信息 24 | */ 25 | @Service 26 | @Slf4j 27 | public class FavoriteListenerTwo { 28 | @Autowired 29 | private UserMapper userMapper; 30 | @Autowired 31 | private UpdateUserInfoUtils updateUserInfoUtils; 32 | @Autowired 33 | private Gson gson; 34 | @Autowired 35 | private StringRedisTemplate redisTemplate; 36 | 37 | /** 38 | * @author zzzi 39 | * @date 2024/3/29 16:49 40 | * 用户关注在这里更新双方的用户信息 41 | * 更新A的点赞数,B的获赞总数 42 | */ 43 | @RabbitListener(queues = {RabbitMQKeys.FAVORITE_USER}) 44 | @Transactional 45 | public void listenToFavorite(@Payload Long[] ids) { 46 | log.info("第二个消费者监听到用户点赞操作,更新用户信息"); 47 | log.info("第二个消费者监听到用户点赞操作,点赞人id为:{}", ids[0]); 48 | log.info("第二个消费者监听到用户点赞操作,更新用户信息,获赞人id为:{}", ids[1]); 49 | //两个用户都更新 50 | UserDO userA = userMapper.selectById(ids[0]); 51 | 52 | 53 | //todo 数据库更新时,尝试加上乐观锁,防止多线程出现问题 54 | //A的点赞数+1 55 | Integer favoriteCount = userA.getFavoriteCount(); 56 | LambdaQueryWrapper queryWrapperA = new LambdaQueryWrapper<>(); 57 | //判断更新时别人是否更新过了 58 | queryWrapperA.eq(UserDO::getFavoriteCount, favoriteCount); 59 | userA.setFavoriteCount(favoriteCount + 1); 60 | int updateA = userMapper.update(userA, queryWrapperA); 61 | if (updateA != 1) { 62 | //更新失败需要重试,手动实现CAS算法 63 | FavoriteListenerOne favoriteListener = (FavoriteListenerOne) AopContext.currentProxy(); 64 | favoriteListener.listenToFavorite(ids); 65 | } 66 | //B的获赞总数+1 67 | UserDO userB = userMapper.selectById(ids[1]); 68 | Long totalFavorited = userB.getTotalFavorited(); 69 | LambdaQueryWrapper queryWrapperB = new LambdaQueryWrapper<>(); 70 | //加上乐观锁 71 | queryWrapperB.eq(UserDO::getTotalFavorited, totalFavorited); 72 | userB.setTotalFavorited(totalFavorited + 1); 73 | /**@author zzzi 74 | * @date 2024/4/14 17:11 75 | * 当前用户的获赞总数超过1W就将其保存到缓存中,认为是大V 76 | */ 77 | if (totalFavorited + 1 >= 10000) 78 | redisTemplate.opsForSet().add(RedisKeys.USER_HOT, ids[1].toString()); 79 | int updateB = userMapper.update(userB, queryWrapperB); 80 | if (updateB != 1) { 81 | //更新失败需要重试,手动实现CAS算法 82 | FavoriteListenerOne favoriteListener = (FavoriteListenerOne) AopContext.currentProxy(); 83 | favoriteListener.listenToFavorite(ids); 84 | } 85 | 86 | //更新两个用户的缓存信息 87 | 88 | String userAJson = gson.toJson(userA); 89 | String userBJson = gson.toJson(userB); 90 | 91 | updateUserInfoUtils.updateUserInfoCache(userA.getUserId(), userAJson); 92 | updateUserInfoUtils.updateUserInfoCache(userB.getUserId(), userBJson); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/FollowListener.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.constant.RedisKeys; 7 | import com.zzzi.common.exception.FollowException; 8 | import com.zzzi.userservice.entity.UserDO; 9 | import com.zzzi.userservice.entity.UserFollowDO; 10 | import com.zzzi.userservice.mapper.UserMapper; 11 | import com.zzzi.common.utils.UpdateUserInfoUtils; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.amqp.core.ExchangeTypes; 14 | import org.springframework.amqp.rabbit.annotation.Exchange; 15 | import org.springframework.amqp.rabbit.annotation.Queue; 16 | import org.springframework.amqp.rabbit.annotation.QueueBinding; 17 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 18 | import org.springframework.aop.framework.AopContext; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.data.redis.core.StringRedisTemplate; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.transaction.annotation.Transactional; 23 | 24 | 25 | /** 26 | * @author zzzi 27 | * @date 2024/3/29 16:38 28 | * 在这里异步的更新用户的基本信息 29 | */ 30 | @Service 31 | @Slf4j 32 | public class FollowListener { 33 | @Autowired 34 | private UserMapper userMapper; 35 | @Autowired 36 | private UpdateUserInfoUtils updateUserInfoUtils; 37 | @Autowired 38 | private Gson gson; 39 | @Autowired 40 | private StringRedisTemplate redisTemplate; 41 | 42 | 43 | /** 44 | * @author zzzi 45 | * @date 2024/3/29 16:49 46 | * 用户关注在这里更新双方的用户信息 47 | */ 48 | @RabbitListener( 49 | bindings = @QueueBinding( 50 | value = @Queue(name = "direct.follow"), 51 | exchange = @Exchange(name = RabbitMQKeys.FOLLOW_EXCHANGE, type = ExchangeTypes.DIRECT), 52 | key = {RabbitMQKeys.FOLLOW_KEY} 53 | ) 54 | ) 55 | @Transactional 56 | public void listenToFollow(String userFollowDOJson) { 57 | log.info("监听到用户关注操作"); 58 | //将接收到的实体转换成实体类 59 | UserFollowDO userFollowDO = gson.fromJson(userFollowDOJson, UserFollowDO.class); 60 | 61 | //得到关注者和被关注者的id 62 | Long followerId = userFollowDO.getFollowerId(); 63 | Long followedId = userFollowDO.getFollowedId(); 64 | 65 | //得到关注者的信息 66 | UserDO follower = userMapper.selectById(followerId); 67 | 68 | //更新关注者的关注数 69 | Integer followCount = follower.getFollowCount(); 70 | LambdaQueryWrapper followWrapper = new LambdaQueryWrapper<>(); 71 | //加上乐观锁 72 | followWrapper.eq(UserDO::getFollowCount, followCount); 73 | follower.setFollowCount(followCount + 1); 74 | //更新关注者的关注数量 75 | int updateFollower = userMapper.update(follower, followWrapper); 76 | if (updateFollower != 1) { 77 | //手动实现CAS算法 78 | FollowListener followListener = (FollowListener) AopContext.currentProxy(); 79 | followListener.listenToFollow(userFollowDOJson); 80 | } 81 | //更新被关注者的粉丝数 82 | //得到被关注者的信息 83 | UserDO followed = userMapper.selectById(followedId); 84 | Integer followerCount = followed.getFollowerCount(); 85 | LambdaQueryWrapper followedWrapper = new LambdaQueryWrapper<>(); 86 | followedWrapper.eq(UserDO::getFollowerCount, followerCount); 87 | followed.setFollowerCount(followerCount + 1); 88 | /**@author zzzi 89 | * @date 2024/4/14 17:21 90 | * 粉丝关注量超过1W就将用户添加到大V列表中,认为是大V 91 | */ 92 | if (followerCount + 1 >= 10000) { 93 | redisTemplate.opsForSet().add(RedisKeys.USER_HOT, followed.getUserId().toString()); 94 | } 95 | int updateFollowed = userMapper.update(followed, followedWrapper); 96 | if (updateFollowed != 1) { 97 | throw new FollowException("用户关注失败"); 98 | } 99 | 100 | //调用方法更新用户缓存 101 | String followerJson = gson.toJson(follower); 102 | String followedJson = gson.toJson(followed); 103 | 104 | updateUserInfoUtils.updateUserInfoCache(followerId, followerJson); 105 | updateUserInfoUtils.updateUserInfoCache(followedId, followedJson); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/PostVideoListener.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.userservice.entity.UserDO; 7 | import com.zzzi.userservice.mapper.UserMapper; 8 | import com.zzzi.common.utils.UpdateUserInfoUtils; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.amqp.core.ExchangeTypes; 11 | import org.springframework.amqp.rabbit.annotation.Exchange; 12 | import org.springframework.amqp.rabbit.annotation.Queue; 13 | import org.springframework.amqp.rabbit.annotation.QueueBinding; 14 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 15 | import org.springframework.aop.framework.AopContext; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.messaging.handler.annotation.Payload; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | 22 | /** 23 | * @author zzzi 24 | * @date 2024/3/27 15:44 25 | * 在这里监听投稿的操作,便于更新用户表中的作品数 26 | * 并且更新缓存 27 | */ 28 | @Service 29 | @Slf4j 30 | public class PostVideoListener { 31 | 32 | @Autowired 33 | private UserMapper userMapper; 34 | @Autowired 35 | private UpdateUserInfoUtils updateUserInfoUtils; 36 | @Autowired 37 | private Gson gson; 38 | 39 | 40 | /** 41 | * @author zzzi 42 | * @date 2024/3/27 16:26 43 | * 在这里需要修改用户表中的作品数 44 | * 以及修改用户缓存中的数据 45 | *

46 | * 这里需要互斥锁,因为修改缓存需要互斥 47 | */ 48 | @RabbitListener( 49 | bindings = @QueueBinding( 50 | value = @Queue(name = "direct.post_video"), 51 | exchange = @Exchange(name = RabbitMQKeys.POST_VIDEO_EXCHANGE, type = ExchangeTypes.DIRECT), 52 | key = {RabbitMQKeys.VIDEO_POST} 53 | ) 54 | ) 55 | @Transactional 56 | public void listenToPostVideo(@Payload Long authorId) { 57 | log.info("监听到用户投稿操作的用户id为:{}", authorId); 58 | //查询得到用户原有信息 59 | UserDO userDO = userMapper.selectById(authorId); 60 | Integer workCount = userDO.getWorkCount(); 61 | 62 | //更新用户作品信息 63 | userDO.setWorkCount(workCount + 1); 64 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 65 | //加上乐观锁 66 | queryWrapper.eq(UserDO::getWorkCount, workCount); 67 | //更新用户表中的作品数 68 | int update = userMapper.update(userDO, queryWrapper); 69 | if (update != 1) { 70 | //手动实现CAS算法 71 | PostVideoListener postVideoListener = (PostVideoListener) AopContext.currentProxy(); 72 | postVideoListener.listenToPostVideo(authorId); 73 | } 74 | 75 | 76 | String userDOJson = gson.toJson(userDO); 77 | //更新用户的缓存 78 | updateUserInfoUtils.updateUserInfoCache(authorId, userDOJson); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/UnFavoriteListenerOne.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.userservice.entity.UserDO; 7 | import com.zzzi.userservice.mapper.UserMapper; 8 | import com.zzzi.common.utils.UpdateUserInfoUtils; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 11 | import org.springframework.aop.framework.AopContext; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.messaging.handler.annotation.Payload; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | 18 | /** 19 | * @author zzzi 20 | * @date 2024/3/29 16:38 21 | * 在这里异步的更新用户的基本信息 22 | */ 23 | @Service 24 | @Slf4j 25 | public class UnFavoriteListenerOne { 26 | @Autowired 27 | private UserMapper userMapper; 28 | @Autowired 29 | private UpdateUserInfoUtils updateUserInfoUtils; 30 | @Autowired 31 | private Gson gson; 32 | 33 | 34 | /** 35 | * @author zzzi 36 | * @date 2024/3/29 16:49 37 | * 用户关注在这里更新双方的用户信息 38 | */ 39 | @RabbitListener(queues = {RabbitMQKeys.UN_FAVORITE_USER}) 40 | @Transactional 41 | public void listenToUnFavorite(@Payload long[] ids) { 42 | log.info("第一个消费者监听到用户取消点赞操作,更新用户信息"); 43 | //两个用户都更新 44 | UserDO userA = userMapper.selectById(ids[0]); 45 | 46 | //A的点赞数+1 47 | Integer favoriteCount = userA.getFavoriteCount(); 48 | LambdaQueryWrapper queryWrapperA = new LambdaQueryWrapper<>(); 49 | //加上乐观锁,判断当前更新时查到的数据是否被其他线程更新过了 50 | queryWrapperA.eq(UserDO::getFavoriteCount, favoriteCount); 51 | userA.setFavoriteCount(favoriteCount - 1); 52 | int updateA = userMapper.update(userA, queryWrapperA); 53 | if (updateA != 1) { 54 | //手动实现CAS算法 55 | UnFavoriteListenerOne UnFavoriteListener = (UnFavoriteListenerOne) AopContext.currentProxy(); 56 | UnFavoriteListener.listenToUnFavorite(ids); 57 | } 58 | 59 | 60 | //B的获赞总数+1 61 | UserDO userB = userMapper.selectById(ids[1]); 62 | Long totalFavorited = userB.getTotalFavorited(); 63 | LambdaQueryWrapper queryWrapperB = new LambdaQueryWrapper<>(); 64 | //加上乐观锁,判断当前更新时查到的数据是否被其他线程更新过了 65 | queryWrapperB.eq(UserDO::getTotalFavorited, totalFavorited); 66 | userB.setTotalFavorited(totalFavorited - 1); 67 | /**@author zzzi 68 | * @date 2024/4/14 17:20 69 | * 获赞总数小于1W时从大V列表中删除 70 | */ 71 | if (totalFavorited - 1 < 10000) { 72 | updateUserInfoUtils.deleteHotUserFormCache(userB.getUserId()); 73 | } 74 | int updateB = userMapper.update(userB, queryWrapperB); 75 | if (updateB != 1) { 76 | //手动实现CAS算法 77 | UnFavoriteListenerOne UnFavoriteListener = (UnFavoriteListenerOne) AopContext.currentProxy(); 78 | UnFavoriteListener.listenToUnFavorite(ids); 79 | } 80 | 81 | //更新两个用户的缓存信息 82 | String userAJson = gson.toJson(userA); 83 | String userBJson = gson.toJson(userB); 84 | 85 | updateUserInfoUtils.updateUserInfoCache(userA.getUserId(), userAJson); 86 | updateUserInfoUtils.updateUserInfoCache(userB.getUserId(), userBJson); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/UnFavoriteListenerTwo.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.userservice.entity.UserDO; 7 | import com.zzzi.userservice.mapper.UserMapper; 8 | import com.zzzi.common.utils.UpdateUserInfoUtils; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 11 | import org.springframework.aop.framework.AopContext; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.messaging.handler.annotation.Payload; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | 18 | /** 19 | * @author zzzi 20 | * @date 2024/3/29 16:38 21 | * 在这里异步的更新用户的基本信息 22 | */ 23 | @Service 24 | @Slf4j 25 | public class UnFavoriteListenerTwo { 26 | @Autowired 27 | private UserMapper userMapper; 28 | @Autowired 29 | private UpdateUserInfoUtils updateUserInfoUtils; 30 | @Autowired 31 | private Gson gson; 32 | 33 | 34 | /** 35 | * @author zzzi 36 | * @date 2024/3/29 16:49 37 | * 用户关注在这里更新双方的用户信息 38 | */ 39 | @RabbitListener(queues = {RabbitMQKeys.UN_FAVORITE_USER}) 40 | @Transactional 41 | public void listenToUnFavorite(@Payload long[] ids) { 42 | log.info("第二个消费者监听到用户取消点赞操作,更新用户信息"); 43 | //两个用户都更新 44 | UserDO userA = userMapper.selectById(ids[0]); 45 | 46 | //A的点赞数+1 47 | Integer favoriteCount = userA.getFavoriteCount(); 48 | LambdaQueryWrapper queryWrapperA = new LambdaQueryWrapper<>(); 49 | //加上乐观锁,判断当前更新时查到的数据是否被其他线程更新过了 50 | queryWrapperA.eq(UserDO::getFavoriteCount, favoriteCount); 51 | //todo:两步自旋异常更新数据库 52 | userA.setFavoriteCount(favoriteCount - 1); 53 | int updateA = userMapper.update(userA, queryWrapperA); 54 | if (updateA != 1) { 55 | //手动实现CAS算法 56 | UnFavoriteListenerOne UnFavoriteListener = (UnFavoriteListenerOne) AopContext.currentProxy(); 57 | UnFavoriteListener.listenToUnFavorite(ids); 58 | } 59 | 60 | 61 | //B的获赞总数+1 62 | UserDO userB = userMapper.selectById(ids[1]); 63 | Long totalFavorited = userB.getTotalFavorited(); 64 | LambdaQueryWrapper queryWrapperB = new LambdaQueryWrapper<>(); 65 | //加上乐观锁,判断当前更新时查到的数据是否被其他线程更新过了 66 | queryWrapperB.eq(UserDO::getTotalFavorited, totalFavorited); 67 | userB.setTotalFavorited(totalFavorited - 1); 68 | /**@author zzzi 69 | * @date 2024/4/14 17:20 70 | * 获赞总数小于1W时从大V列表中删除 71 | */ 72 | if (totalFavorited - 1 < 10000) { 73 | updateUserInfoUtils.deleteHotUserFormCache(userB.getUserId()); 74 | } 75 | int updateB = userMapper.update(userB, queryWrapperB); 76 | if (updateB != 1) { 77 | //手动实现CAS算法 78 | UnFavoriteListenerOne UnFavoriteListener = (UnFavoriteListenerOne) AopContext.currentProxy(); 79 | UnFavoriteListener.listenToUnFavorite(ids); 80 | } 81 | 82 | //更新两个用户的缓存信息 83 | String userAJson = gson.toJson(userA); 84 | String userBJson = gson.toJson(userB); 85 | 86 | updateUserInfoUtils.updateUserInfoCache(userA.getUserId(), userAJson); 87 | updateUserInfoUtils.updateUserInfoCache(userB.getUserId(), userBJson); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/listener/UnFollowListener.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.FollowException; 7 | import com.zzzi.userservice.entity.UserDO; 8 | import com.zzzi.userservice.entity.UserFollowDO; 9 | import com.zzzi.userservice.mapper.UserMapper; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.core.ExchangeTypes; 12 | import com.zzzi.common.utils.UpdateUserInfoUtils; 13 | import org.springframework.amqp.rabbit.annotation.Exchange; 14 | import org.springframework.amqp.rabbit.annotation.Queue; 15 | import org.springframework.amqp.rabbit.annotation.QueueBinding; 16 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 17 | import org.springframework.aop.framework.AopContext; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.messaging.handler.annotation.Payload; 20 | import org.springframework.stereotype.Service; 21 | import org.springframework.transaction.annotation.Transactional; 22 | 23 | /** 24 | * @author zzzi 25 | * @date 2024/3/29 16:59 26 | * 这里执行取消关注的逻辑 27 | */ 28 | @Service 29 | @Slf4j 30 | public class UnFollowListener { 31 | 32 | @Autowired 33 | private UserMapper userMapper; 34 | @Autowired 35 | private UpdateUserInfoUtils updateUserInfoUtils; 36 | @Autowired 37 | private Gson gson; 38 | 39 | 40 | /** 41 | * @author zzzi 42 | * @date 2024/3/29 16:49 43 | * 用户关注在这里更新双方的用户信息 44 | */ 45 | @RabbitListener( 46 | bindings = @QueueBinding( 47 | value = @Queue(name = "direct.un_follow"), 48 | exchange = @Exchange(name = RabbitMQKeys.FOLLOW_EXCHANGE, type = ExchangeTypes.DIRECT), 49 | key = {RabbitMQKeys.UN_FOLLOW_KEY} 50 | ) 51 | ) 52 | @Transactional 53 | public void listenToUnFollow(@Payload String userUnFollowDOJson) { 54 | log.info("监听到用户取消关注"); 55 | //将接收到的实体转换成实体类 56 | UserFollowDO userUnFollowDO = gson.fromJson(userUnFollowDOJson, UserFollowDO.class); 57 | 58 | //得到取消关注者和被取消关注者的id 59 | Long unFollowerId = userUnFollowDO.getFollowerId(); 60 | Long unFollowedId = userUnFollowDO.getFollowedId(); 61 | 62 | //得到取消关注者的信息 63 | UserDO unFollower = userMapper.selectById(unFollowerId); 64 | 65 | //更新取消关注者的关注数 66 | Integer followCount = unFollower.getFollowCount(); 67 | LambdaQueryWrapper followWrapper = new LambdaQueryWrapper<>(); 68 | //加上乐观锁 69 | followWrapper.eq(UserDO::getFollowCount, followCount); 70 | unFollower.setFollowCount(followCount - 1); 71 | int updateUnFollower = userMapper.update(unFollower, followWrapper); 72 | if (updateUnFollower != 1) { 73 | //手动实现CAS算法 74 | UnFollowListener unFollowListener = (UnFollowListener) AopContext.currentProxy(); 75 | unFollowListener.listenToUnFollow(userUnFollowDOJson); 76 | } 77 | 78 | //更新被取消关注者的粉丝数 79 | //得到被取消关注者的信息 80 | UserDO unFollowed = userMapper.selectById(unFollowedId); 81 | Integer followerCount = unFollowed.getFollowerCount(); 82 | LambdaQueryWrapper followedWrapper = new LambdaQueryWrapper<>(); 83 | //加上乐观锁 84 | followedWrapper.eq(UserDO::getFollowerCount, followerCount); 85 | unFollowed.setFollowerCount(followerCount - 1); 86 | /**@author zzzi 87 | * @date 2024/4/14 17:34 88 | * 当前用户的粉丝数小于1W,此时将当前用户从大V列表中删除 89 | */ 90 | if (followerCount - 1 < 10000) { 91 | updateUserInfoUtils.deleteHotUserFormCache(unFollowed.getUserId()); 92 | } 93 | int updateUnFollowed = userMapper.update(unFollowed, followedWrapper); 94 | if (updateUnFollowed != 1) { 95 | //手动实现CAS算法 96 | UnFollowListener unFollowListener = (UnFollowListener) AopContext.currentProxy(); 97 | unFollowListener.listenToUnFollow(userUnFollowDOJson); 98 | } 99 | 100 | //调用方法更新用户缓存 101 | String followerJson = gson.toJson(unFollower);//主动取消关注者 102 | String followedJson = gson.toJson(unFollowed);//被动取消关注者 103 | 104 | updateUserInfoUtils.updateUserInfoCache(unFollowerId, followerJson); 105 | updateUserInfoUtils.updateUserInfoCache(unFollowedId, followedJson); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/mapper/MessageMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.userservice.entity.MessageDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface MessageMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/mapper/RelationMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.userservice.entity.UserFollowDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface RelationMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.userservice.entity.UserDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface UserMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.common.result.MessageVO; 5 | import com.zzzi.userservice.entity.MessageDO; 6 | 7 | import java.util.List; 8 | 9 | public interface MessageService extends IService { 10 | void messageAction(String token, String to_user_id, String content); 11 | 12 | List getMessageList(String token, String to_user_id); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/service/RelationService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.common.result.UserVO; 5 | import com.zzzi.userservice.entity.UserFollowDO; 6 | 7 | import java.util.List; 8 | 9 | public interface RelationService extends IService { 10 | void followAction(String token, Long to_user_id); 11 | 12 | void followUnAction(String token, Long to_user_id1); 13 | 14 | List getFollowList(String user_id, String token); 15 | 16 | List getFollowerList(String user_id, String token); 17 | 18 | List getFriendList(String user_id, String token); 19 | } 20 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.userservice.dto.UserDTO; 5 | import com.zzzi.userservice.entity.UserDO; 6 | import com.zzzi.common.result.UserVO; 7 | 8 | public interface UserService extends IService { 9 | UserDTO login(String username, String password); 10 | 11 | UserDTO register(String username, String password); 12 | 13 | UserVO getUserInfo(String user_id); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /user-service/src/main/java/com/zzzi/userservice/service/impl/MessageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.zzzi.common.result.MessageVO; 6 | import com.zzzi.common.utils.JwtUtils; 7 | import com.zzzi.common.utils.UpdateTokenUtils; 8 | import com.zzzi.userservice.entity.MessageDO; 9 | import com.zzzi.userservice.mapper.MessageMapper; 10 | import com.zzzi.userservice.service.MessageService; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | @Service 20 | @Slf4j 21 | public class MessageServiceImpl extends ServiceImpl implements MessageService { 22 | @Autowired 23 | private MessageMapper messageMapper; 24 | @Autowired 25 | private UpdateTokenUtils updateTokenUtils; 26 | 27 | /** 28 | * @author zzzi 29 | * @date 2024/4/4 14:54 30 | * 直接将当前两个用户的消息保存到数据库中 31 | */ 32 | @Override 33 | @Transactional 34 | public void messageAction(String token, String to_user_id, String content) { 35 | log.info("用户发送消息service,用户token为:{},to_user_id为:{}", token, to_user_id); 36 | //解析得到消息发送方id 37 | Long fromUserId = JwtUtils.getUserIdByToken(token); 38 | Long toUserId = Long.valueOf(to_user_id); 39 | MessageDO messageDO = new MessageDO(); 40 | messageDO.setFromUserId(fromUserId); 41 | messageDO.setToUserId(toUserId); 42 | messageDO.setContent(content); 43 | 44 | //将其插入到数据库中 45 | int insert = messageMapper.insert(messageDO); 46 | if (insert != 1) {//插入失败 47 | throw new RuntimeException("用户发送消息失败"); 48 | } 49 | //更新发送方的token 50 | updateTokenUtils.updateTokenExpireTimeUtils(fromUserId.toString()); 51 | } 52 | 53 | @Override 54 | public List getMessageList(String token, String to_user_id) { 55 | log.info("获取用户消息列表,token为:{},to_user_id为:{}", token, to_user_id); 56 | List message_list = new ArrayList<>(); 57 | //解析得到用户的id 58 | Long fromUserId = JwtUtils.getUserIdByToken(token); 59 | //从数据库中查询得到所有的消息列表 60 | /**@author zzzi 61 | * @date 2024/4/4 16:25 62 | * 这里获取的是 63 | */ 64 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 65 | queryWrapper.eq(MessageDO::getFromUserId, fromUserId).eq(MessageDO::getToUserId, to_user_id); 66 | 67 | //将获得的所有MessageDO打包成MessageVO 68 | List messageDOList = messageMapper.selectList(queryWrapper); 69 | for (MessageDO messageDO : messageDOList) { 70 | MessageVO messageVO = packageMessageVO(messageDO); 71 | message_list.add(messageVO); 72 | } 73 | //更新消息获取方的token 74 | updateTokenUtils.updateTokenExpireTimeUtils(fromUserId.toString()); 75 | return message_list; 76 | } 77 | 78 | /** 79 | * @author zzzi 80 | * @date 2024/4/4 15:01 81 | * 打包一个MessageVO 82 | */ 83 | private MessageVO packageMessageVO(MessageDO messageDO) { 84 | MessageVO messageVO = new MessageVO(); 85 | messageVO.setId(messageDO.getMessageId()); 86 | messageVO.setContent(messageDO.getContent()); 87 | messageVO.setCreate_time(messageDO.getCreateTime().getTime()); 88 | 89 | return messageVO; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /user-service/src/main/resources/application.txt: -------------------------------------------------------------------------------- 1 | spring: 2 | # 数据库相关配置 3 | main: 4 | allow-bean-definition-overriding: true 5 | shardingsphere: 6 | datasource: 7 | ds: 8 | maxPoolSize: 100 9 | # master数据库连接信息 10 | master0: 11 | driver-class-name: com.mysql.jdbc.Driver 12 | maxPoolSize: 100 13 | minPoolSize: 5 14 | username: root 15 | password: 123456 16 | type: com.alibaba.druid.pool.DruidDataSource 17 | url: jdbc:mysql://localhost:3306/tiktok?useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8 18 | # slaver数据库连接信息 19 | slaver0: 20 | driver-class-name: com.mysql.jdbc.Driver 21 | maxPoolSize: 100 22 | minPoolSize: 5 23 | username: root 24 | password: 123456 25 | type: com.alibaba.druid.pool.DruidDataSource 26 | url: jdbc:mysql://localhost:3307/tiktok?useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8 27 | # 配置数据源 28 | names: master0,slaver0 29 | # 显示sql 30 | props: 31 | sql: 32 | show: true 33 | # 配置默认数据源master 默认数据源,主要用于写 34 | sharding: 35 | default-database-strategy: 36 | inline: 37 | sharding-column: user_id 38 | algorithm-expression: master$->{user_id % 1} 39 | # 配置分表策略 40 | tables: 41 | comment: 42 | actual-data-nodes: master$->{0}.comment_$->{1..8} 43 | table-strategy: 44 | inline: 45 | sharding-column: comment_id 46 | algorithm-expression: comment_$->{comment_id % 8 + 1} 47 | favorite: 48 | actual-data-nodes: master$->{0}.favorite_$->{1..8} 49 | table-strategy: 50 | inline: 51 | sharding-column: favorite_id 52 | algorithm-expression: favorite_$->{favorite_id % 8 + 1} 53 | user_follows: 54 | actual-data-nodes: master$->{0}.user_follows_$->{1..4} 55 | table-strategy: 56 | inline: 57 | sharding-column: user_follows_id 58 | algorithm-expression: user_follows_$->{user_follows_id % 4 + 1} 59 | users: 60 | actual-data-nodes: master$->{0}.users_$->{1..2} 61 | table-strategy: 62 | inline: 63 | sharding-column: user_id 64 | algorithm-expression: users_$->{user_id % 2 + 1} 65 | video: 66 | actual-data-nodes: master$->{0}.video_$->{1..8} 67 | table-strategy: 68 | inline: 69 | sharding-column: video_id 70 | algorithm-expression: video_$->{video_id % 8 + 1} 71 | master-slave-rules: 72 | master0: 73 | master-data-source-name: master0 74 | slave-data-source-names: slaver0 75 | application: 76 | name: userservice 77 | mvc: 78 | static-path-pattern:server: /** 79 | cloud: 80 | # nacos相关地址 81 | nacos: 82 | server-addr: localhost:8848 83 | kafka: 84 | bootstrap-servers: localhost:9092 85 | consumer: 86 | group-id: test-consumer-group 87 | enable-auto-commit: true 88 | auto-commit-interval: 3000 89 | 90 | # 腾讯云发送短信的配置 91 | tencent: 92 | sms: 93 | secretId: yourSecretId 94 | secretKey: yourSecretKey 95 | endpoint: yourEndpoint 96 | region: yourRegion 97 | sdkAppId: yourSdkAppId 98 | signName: yourSignName 99 | templateId: yourTemplateId 100 | signMethod: "HmacSHA256" 101 | 102 | rabbitmq: 103 | # 生产者确认类型 104 | publisher-confirm-type: correlated 105 | publisher-returns: true 106 | template: 107 | mandatory: true 108 | host: localhost 109 | port: 5672 110 | username: root 111 | password: 123456 112 | virtual-host: / # 虚拟主机 113 | listener: 114 | direct: 115 | prefetch: 1 # 每个消费者每次只消费一条消息,不会出现高消费的情况,也就是承担不了那么多消息 116 | simple: 117 | # 消费者消息确认机制 118 | acknowledge-mode: auto 119 | retry: 120 | # 开启消费者失败重试 121 | enabled: true 122 | # 初始的失败等待时长为1秒 123 | initial-interval: 1000 124 | # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval 125 | multiplier: 1 126 | # 最大重试次数 127 | max-attempts: 3 128 | # true无状态;false有状态。如果业务中包含事务,这里改为false 129 | stateless: false 130 | # 最大文件大小 131 | servlet: 132 | multipart: 133 | max-file-size: 100MB 134 | max-request-size: 100MB 135 | redis: 136 | host: localhost 137 | port: 6379 138 | lettuce: 139 | cluster: 140 | refresh: 141 | adaptive: true 142 | period: 600000 143 | pool: 144 | max-active: 8 # 连接池最大连接数 145 | max-wait: -1 # 最大阻塞等待时间没有限制 146 | min-idle: 0 # 连接池中最小空闲连接数 147 | timeout: 200 # 连接超时时间 148 | server: 149 | port: 9191 150 | # 缓存过期值随机打散(30分钟到60分钟) 151 | random_start: 30 152 | random_end: 60 153 | 154 | mybatis-plus: 155 | configuration: 156 | #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 157 | map-underscore-to-camel-case: true 158 | # 日志输出到控制台 159 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 160 | logging: 161 | level: 162 | # 这个包下的日志记录级别为debug 163 | com.zzzi: debug 164 | pattern: 165 | dateformat: MM-dd HH:mm:ss:SSS 166 | feign: 167 | httpclient: 168 | enabled: true # 支持HttpClient的开关 169 | max-connections: 200 # 最大连接数 170 | max-connections-per-route: 50 # 单个路径的最大连接数 171 | # 防止远程调用出错 172 | ribbon.nacos.enabled: true -------------------------------------------------------------------------------- /user-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.GREEN} 2 | ██╗ ██╗███████╗███████╗██████╗ 3 | ██║ ██║██╔════╝██╔════╝██╔══██╗ 4 | ██║ ██║███████╗█████╗ ██████╔╝ 5 | ██║ ██║╚════██║██╔══╝ ██╔══██╗ 6 | ╚██████╔╝███████║███████╗██║ ██║ 7 | ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ 8 | ${AnsiColor.BRIGHT_BLACK} -------------------------------------------------------------------------------- /user-service/src/test/java/com/zzzi/userservice/UserServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.userservice; 2 | 3 | import com.alibaba.csp.sentinel.util.StringUtil; 4 | import com.zzzi.common.utils.JwtUtils; 5 | import com.zzzi.common.utils.MD5Utils; 6 | import org.apache.commons.lang.StringUtils; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 | 11 | import java.util.UUID; 12 | 13 | //@SpringBootTest 14 | class UserServiceApplicationTests { 15 | 16 | @Test 17 | void contextLoads() { 18 | } 19 | 20 | /** 21 | * @author zzzi 22 | * @date 2024/3/27 13:57 23 | * 测试用户名和用户id一致时,前后生成的token是否一致 24 | */ 25 | @Test 26 | void testToken() { 27 | Long userId = 1500000000000000000L; 28 | String userName = "123"; 29 | String token1 = JwtUtils.createToken(userId, userName); 30 | String token2 = JwtUtils.createToken(userId, userName); 31 | System.out.println(token1); 32 | System.out.println(token2); 33 | System.out.println(token1.equals(token2)); 34 | } 35 | 36 | @Test 37 | void testParseToken() { 38 | String token = "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWKi5NUrJSconUDQ12DVLSUUqtKFCyMjQ3NLSwsDAytdBRKi1OLfJMAYqZGqADiKRfYm4q0AhDI2OlWgAB1h9IUAAAAA.1S3xfjQNKSeR6ytrMN3tJBh9CkH4qOINVWMCAWXJZGJF5SzU6nMRyejpfLQbQ0iTQXrbjh_uN6UJKWRiMY28Ww"; 39 | Long userIdByToken = JwtUtils.getUserIdByToken(token); 40 | System.out.println(userIdByToken); 41 | } 42 | 43 | @Test 44 | void testBCryptPasswordEncoder() { 45 | // Create an encoder with strength 16 46 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); 47 | String result = encoder.encode("123456"); 48 | System.out.println(result); 49 | } 50 | 51 | @Test 52 | void testMD5Salt() { 53 | String passMD5 = MD5Utils.parseStrToMd5L32("123456"); 54 | System.out.println(passMD5); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /video-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | *.yml 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/VideoServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice; 2 | 3 | import com.alibaba.cloud.nacos.ribbon.NacosRule; 4 | import com.netflix.loadbalancer.IRule; 5 | import com.zzzi.common.config.DefaultFeignConfiguration; 6 | import com.zzzi.common.constant.RedisKeys; 7 | import com.zzzi.common.feign.UserClient; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.boot.autoconfigure.SpringBootApplication; 12 | import org.springframework.cloud.openfeign.EnableFeignClients; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 15 | import org.springframework.data.redis.core.StringRedisTemplate; 16 | import org.springframework.transaction.annotation.EnableTransactionManagement; 17 | 18 | import javax.annotation.PreDestroy; 19 | import java.util.Set; 20 | 21 | @SpringBootApplication(scanBasePackages = {"com.zzzi.*"}) 22 | @EnableTransactionManagement//开启事务管理 23 | @EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class) 24 | @EnableAspectJAutoProxy(exposeProxy = true) 25 | public class VideoServiceApplication { 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(VideoServiceApplication.class, args); 29 | } 30 | 31 | //负载均衡规则 32 | @Bean 33 | public IRule rule() { 34 | return new NacosRule(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/BinLogEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | 4 | import com.gitee.Jmysy.binlog4j.core.BinlogEvent; 5 | import com.gitee.Jmysy.binlog4j.core.IBinlogEventHandler; 6 | import com.gitee.Jmysy.binlog4j.springboot.starter.annotation.BinlogSubscriber; 7 | import com.zzzi.common.constant.RedisDefaultValue; 8 | import com.zzzi.common.constant.RedisKeys; 9 | import com.zzzi.videoservice.entity.VideoDO; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.data.redis.core.StringRedisTemplate; 14 | 15 | import java.util.List; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * @author zzzi 20 | * @date 2024/4/8 12:10 21 | * 在这里监听mysql中binlog的变化,从而完成缓存同步 22 | */ 23 | @Slf4j 24 | @BinlogSubscriber(clientName = "master") 25 | public class BinLogEventHandler implements IBinlogEventHandler { 26 | 27 | @Autowired 28 | private StringRedisTemplate redisTemplate; 29 | 30 | @Value("${user_works_max_size}") 31 | public Long USER_WORKS_MAX_SIZE; 32 | 33 | @Override 34 | public void onInsert(BinlogEvent binlogEvent) { 35 | log.info("监听到视频表的插入"); 36 | Long authorId = binlogEvent.getData().getAuthorId(); 37 | Long videoId = binlogEvent.getData().getVideoId(); 38 | 39 | //如果用户作品列表中有默认值,此时先删除默认值再添加 40 | List userWorkList = redisTemplate.opsForList().range(RedisKeys.USER_WORKS_PREFIX + authorId, 0, -1); 41 | if (userWorkList.contains(RedisDefaultValue.REDIS_DEFAULT_VALUE)) { 42 | log.info("删除用户作品列表缓存的默认值"); 43 | redisTemplate.delete(RedisKeys.USER_WORKS_PREFIX + authorId); 44 | } 45 | //先插入当前投稿的作品信息 46 | redisTemplate.opsForList().leftPush(RedisKeys.USER_WORKS_PREFIX + authorId, videoId + ""); 47 | while (redisTemplate.opsForList().size(RedisKeys.USER_WORKS_PREFIX + authorId) > USER_WORKS_MAX_SIZE) { 48 | //从右边删除,代表删除最早投稿的视频 49 | redisTemplate.opsForList().rightPop(RedisKeys.USER_WORKS_PREFIX + authorId); 50 | } 51 | } 52 | 53 | //todo:用户信息和视频信息更新后,尝试直接更新缓存 54 | @Override 55 | public void onUpdate(BinlogEvent binlogEvent) { 56 | log.info("监听到视频表的更新"); 57 | VideoDO data = binlogEvent.getData(); 58 | 59 | System.out.println(data); 60 | } 61 | 62 | @Override 63 | public void onDelete(BinlogEvent binlogEvent) { 64 | log.info("监听到视频表的删除"); 65 | VideoDO data = binlogEvent.getData(); 66 | 67 | System.out.println(data); 68 | 69 | } 70 | 71 | @Override 72 | public boolean isHandle(String s, String s1) { 73 | log.info("监听的数据库为:{},表名为:{}", s, s1); 74 | //变化的表是tiktok中的video_{1..8}是才触发当前handler的执行 75 | return s.equals("tiktok") && Pattern.matches("^video_[1-8]$", s1); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/MyMetaObjectHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import com.zzzi.common.exception.VideoException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.ibatis.reflection.MetaObject; 7 | import org.springframework.stereotype.Component; 8 | 9 | /**@author zzzi 10 | * @date 2024/3/27 19:25 11 | * 在这里进行基本的属性回显 12 | * 更加复杂的属性回显后期操作 13 | * 所有实体的内容都可以在这里重新赋值,相当于共用一个填充器 14 | */ 15 | import java.time.LocalDateTime; 16 | import java.util.Date; 17 | 18 | @Component 19 | @Slf4j 20 | public class MyMetaObjectHandler implements MetaObjectHandler { 21 | 22 | @Override 23 | public void insertFill(MetaObject metaObject) { 24 | try { 25 | //todo 实体类的id使用雪花算法生成全局唯一ID 26 | this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); 27 | this.strictInsertFill(metaObject, "title", String.class, "抖音记录美好生活"); 28 | //更新时间不管什么时候都自动更新 29 | this.strictInsertFill(metaObject, "updateTime", Date.class, new Date()); 30 | this.strictInsertFill(metaObject, "favoriteCount", Integer.class, 0); 31 | this.strictInsertFill(metaObject, "commentCount", Integer.class, 0); 32 | } catch (Exception e) { 33 | log.error(e.getMessage()); 34 | throw new VideoException("属性自动填充失败"); 35 | } 36 | } 37 | 38 | @Override 39 | public void updateFill(MetaObject metaObject) { 40 | try { 41 | //更新时间不管如何都直接填充 42 | this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); 43 | } catch (Exception e) { 44 | log.error(e.getMessage()); 45 | throw new VideoException("属性自动填充失败"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/MybatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * @author zzzi 11 | * @date 2024/4/2 17:07 12 | * 在这里配置分页插件 13 | */ 14 | @Configuration 15 | public class MybatisPlusConfig { 16 | 17 | /** 18 | * 添加分页插件 19 | */ 20 | @Bean 21 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 22 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 23 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加 24 | return interceptor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/RabbitMQConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import com.zzzi.common.constant.RabbitMQKeys; 4 | import org.springframework.amqp.core.Binding; 5 | import org.springframework.amqp.core.BindingBuilder; 6 | import org.springframework.amqp.core.DirectExchange; 7 | import org.springframework.amqp.core.Queue; 8 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 9 | import org.springframework.amqp.rabbit.retry.MessageRecoverer; 10 | import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; 11 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 12 | import org.springframework.amqp.support.converter.MessageConverter; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | /** 17 | * @author zzzi 18 | * @date 2024/3/27 16:24 19 | * 这可以使得RabbitMQ使用json序列化,而不是使用java中的jdk序列化 20 | */ 21 | @Configuration 22 | public class RabbitMQConfig { 23 | @Bean 24 | public MessageConverter jsonMessageConverter() { 25 | return new Jackson2JsonMessageConverter(); 26 | } 27 | 28 | /** 29 | * @author zzzi 30 | * @date 2024/4/4 19:09 31 | * 定义两个Work模式的队列,监听点赞消息,用来更新视频信息 32 | */ 33 | @Bean 34 | public Queue createFavoriteVideoQueue() { 35 | return new Queue(RabbitMQKeys.FAVORITE_VIDEO); 36 | } 37 | 38 | @Bean 39 | public Queue createUnFavoriteVideoQueue() { 40 | return new Queue(RabbitMQKeys.UN_FAVORITE_VIDEO); 41 | } 42 | 43 | /** 44 | * @author zzzi 45 | * @date 2024/4/9 16:01 46 | * 消费者消费消息失败时会进行重试 47 | * 重试次数达到设置的上限时会将失败的消息投放到这个队列中 48 | */ 49 | 50 | //定义错误消息的交换机 51 | @Bean 52 | public DirectExchange errorMessageExchange() { 53 | return new DirectExchange(RabbitMQKeys.ERROR_EXCHANGE); 54 | } 55 | 56 | //定义错误消息的队列 57 | @Bean 58 | public Queue errorQueue() { 59 | return new Queue("error.queue", true); 60 | } 61 | 62 | //定义二者之间的绑定关系 63 | @Bean 64 | public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange) { 65 | return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with(RabbitMQKeys.ERROR); 66 | } 67 | 68 | @Bean 69 | public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) { 70 | //重试次数耗尽时,会将失败的消息发送给指定的队列 71 | return new RepublishMessageRecoverer(rabbitTemplate, RabbitMQKeys.ERROR_EXCHANGE, RabbitMQKeys.ERROR); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/ReturnCallBack.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.context.ApplicationContextAware; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | 11 | @Configuration 12 | @Slf4j 13 | /**@author zzzi 14 | * @date 2024/4/9 15:24 15 | * 定义消息发送失败的回调 16 | * 消息到达交换机但是没有到达队列时触发 17 | */ 18 | public class ReturnCallBack implements ApplicationContextAware { 19 | @Override 20 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 21 | // 获取RabbitTemplate 22 | RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); 23 | // 设置ReturnCallback 24 | rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { 25 | // 投递失败,记录日志 26 | log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}", 27 | replyCode, replyText, exchange, routingKey, message.toString()); 28 | // 如果有业务需要,可以重发消息 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/UnExpectedExitHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import com.zzzi.common.constant.RedisKeys; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.DisposableBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Set; 11 | 12 | /** 13 | * @author zzzi 14 | * @date 2024/4/4 17:27 15 | * 程序意外退出执行这个方法 16 | */ 17 | @Component 18 | @Slf4j 19 | public class UnExpectedExitHandler implements DisposableBean { 20 | @Autowired 21 | private StringRedisTemplate redisTemplate; 22 | 23 | /** 24 | * @author zzzi 25 | * @date 2024/4/4 17:22 26 | * 系统崩溃时删除所有用户token,防止token没过期下次登录不上 27 | */ 28 | @Override 29 | public void destroy() throws Exception { 30 | boolean state = false; 31 | Set keys = redisTemplate.keys("*"); 32 | for (String key : keys) { 33 | //只删除用户token 34 | if (key.startsWith(RedisKeys.USER_TOKEN_PREFIX)) 35 | redisTemplate.delete(key); 36 | state = true; 37 | } 38 | if (state) { 39 | log.info("清除缓存成功!"); 40 | } else { 41 | log.info("无缓存数据可清除!"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.config; 2 | 3 | import com.zzzi.videoservice.interceptor.LoginUserInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | /** 9 | * @author zzzi 10 | * @date 2024/3/29 15:23 11 | * 给当前项目配置拦截器 12 | */ 13 | @Configuration 14 | public class WebConfig implements WebMvcConfigurer { 15 | 16 | @Override 17 | public void addInterceptors(InterceptorRegistry registry) { 18 | // 只拦截需要 登录校验的请求.比如登录/评论/点赞/发布视频/用户关注/推送视频 19 | // 除了登录和获取资源还有推送视频不拦截,其他的都拦截 20 | registry.addInterceptor(new LoginUserInterceptor()).excludePathPatterns( 21 | "/douyin/user/**",//这里包括获取用户信息 22 | "/resource/**", 23 | "/douyin/feed/**" 24 | ).order(1); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.controller; 2 | 3 | import com.zzzi.common.result.CommentActionVO; 4 | import com.zzzi.common.result.CommentListVO; 5 | import com.zzzi.common.result.CommentVO; 6 | import com.zzzi.videoservice.service.CommentService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | //评论模块 14 | @RestController 15 | @RequestMapping("/douyin/comment") 16 | @Slf4j 17 | public class CommentController { 18 | 19 | @Autowired 20 | private CommentService commentService; 21 | 22 | /** 23 | * @author zzzi 24 | * @date 2024/4/2 22:17 25 | * 获取视频评论列表 26 | * 这里的token应该不是必须的,没有登录的时候看视频评论,用户is_follow默认都是false,视频也没有点赞 27 | * 先当他是必须的 28 | */ 29 | @GetMapping("/list") 30 | public CommentListVO getCommentList(String token, String video_id) { 31 | log.info("获取视频评论列表,token为:{},video_id为:{}", token, video_id); 32 | //截取真正的token 33 | if (token != null && token.startsWith("login:token:")) 34 | token = token.substring(12); 35 | List comment_list = commentService.getCommentList(token, video_id); 36 | if (comment_list != null) { 37 | return CommentListVO.success("获取评论列表成功", comment_list); 38 | } 39 | return CommentListVO.fail("获取评论列表失败"); 40 | } 41 | 42 | /** 43 | * @author zzzi 44 | * @date 2024/5/5 20:35 45 | * 获取指定父评论的子评论(点击查看更多发的请求) 46 | */ 47 | @GetMapping("/listParent") 48 | public CommentListVO getParentCommentList(String token, String parent_id) { 49 | log.info("获取视频评论列表,token为:{},parent_id为:{}", token, parent_id); 50 | //截取真正的token 51 | if (token != null && token.startsWith("login:token:")) 52 | token = token.substring(12); 53 | List comment_list = commentService.getParentCommentList(token, parent_id); 54 | if (comment_list != null) { 55 | return CommentListVO.success("获取父评论列表成功", comment_list); 56 | } 57 | return CommentListVO.fail("获取父评论列表失败"); 58 | } 59 | 60 | /** 61 | * @author zzzi 62 | * @date 2024/5/5 16:17 63 | * 用户父子评论操作 64 | */ 65 | @PostMapping("/action") 66 | public CommentActionVO commentParentAction(String token, String video_id, String action_type, 67 | @RequestParam(required = false) String comment_text, 68 | @RequestParam(required = false) String comment_id, 69 | @RequestParam(required = false) String parent_id, 70 | @RequestParam(required = false) String reply_id) { 71 | log.info("用户评论操作,token 为:{},video_id为:{},action_type为:{},comment_id为:{}", token, video_id, action_type, comment_id); 72 | //截取真正的token 73 | if (token.startsWith("login:token:")) 74 | token = token.substring(12); 75 | if ("1".equals(action_type)) {//评论操作 76 | CommentVO commentVO = commentService.commentParentAction(token, video_id, comment_text, parent_id, reply_id); 77 | return commentVO != null ? CommentActionVO.success("评论成功", commentVO) : 78 | CommentActionVO.fail("评论失败"); 79 | } else {//删除评论操作 80 | CommentVO commentVO = commentService.commentParentUnAction(token, video_id, comment_id); 81 | return commentVO != null ? CommentActionVO.success("删除评论成功", commentVO) : 82 | CommentActionVO.fail("删除评论失败"); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/controller/FavoriteController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.controller; 2 | 3 | import com.alibaba.csp.sentinel.annotation.SentinelResource; 4 | import com.zzzi.common.result.CommonVO; 5 | import com.zzzi.common.result.VideoListVO; 6 | import com.zzzi.common.result.VideoVO; 7 | import com.zzzi.videoservice.service.FavoriteService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | 17 | //用户点赞的相关操作 18 | @RestController 19 | @RequestMapping("/douyin/favorite") 20 | @Slf4j 21 | public class FavoriteController { 22 | 23 | @Autowired 24 | private FavoriteService favoriteService; 25 | 26 | /** 27 | * @author zzzi 28 | * @date 2024/4/2 12:50 29 | * 用户点赞/取消点赞 30 | * 失败时会在service层报错 31 | */ 32 | @PostMapping("/action") 33 | public CommonVO favoriteAction(String token, String video_id, String action_type) { 34 | log.info("用户点赞操作service,token为:{},video_id为:{},action_type为:{}", token, video_id, action_type); 35 | //截取真正的token 36 | if (token.startsWith("login:token:")) 37 | token = token.substring(12); 38 | String status_msg = ""; 39 | if ("1".equals(action_type)) { 40 | log.info("用户点赞"); 41 | favoriteService.favoriteAction(token, video_id); 42 | status_msg = "成功点赞"; 43 | } else { 44 | log.info("用户取消点赞"); 45 | favoriteService.favoriteUnAction(token, video_id); 46 | status_msg = "成功取消点赞"; 47 | } 48 | return CommonVO.success(status_msg); 49 | } 50 | 51 | /** 52 | * @author zzzi 53 | * @date 2024/4/2 12:50 54 | * 获取用户喜欢列表 55 | */ 56 | @SentinelResource("userFavorites") 57 | @GetMapping("/list") 58 | public VideoListVO getFavoriteList(String user_id, String token) { 59 | log.info("获取用户点赞列表,user_id为:{},token为:{}", user_id, token); 60 | //截取所有的token 61 | if (token != null && token.startsWith("login:token:")) 62 | token = token.substring(12); 63 | List videoVOList = favoriteService.getFavoriteList(user_id, token); 64 | if (videoVOList != null) { 65 | return VideoListVO.success("成功", videoVOList); 66 | } 67 | return VideoListVO.fail("获取用户点赞列表失败"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/controller/VideoController.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.controller; 2 | 3 | import com.alibaba.csp.sentinel.annotation.SentinelResource; 4 | import com.zzzi.common.result.CommonVO; 5 | import com.zzzi.common.result.VideoFeedListVO; 6 | import com.zzzi.common.result.VideoListVO; 7 | import com.zzzi.common.result.VideoVO; 8 | import com.zzzi.common.utils.MultiPartUploadUtils; 9 | import com.zzzi.videoservice.dto.VideoFeedDTO; 10 | import com.zzzi.videoservice.service.VideoService; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.web.bind.annotation.*; 14 | import org.springframework.web.multipart.MultipartFile; 15 | 16 | import javax.annotation.PostConstruct; 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.List; 20 | import java.util.UUID; 21 | 22 | @RestController 23 | @RequestMapping("/douyin") 24 | @Slf4j 25 | public class VideoController { 26 | @Autowired 27 | private VideoService videoService; 28 | 29 | /** 30 | * @author zzzi 31 | * @date 2024/3/28 14:13 32 | * 获取用户的所有作品 33 | * 由于作品信息更新时会删除缓存,所以可能需要缓存重构 34 | * 并且作品列表涉及到获取用户信息,所以需要远程调用 35 | */ 36 | @SentinelResource("userWorks") 37 | @GetMapping("/publish/list") 38 | public VideoListVO getPublishList(String token, Long user_id) { 39 | log.info("获取用户投稿列表,token为:{},user_id为:{}", token, user_id); 40 | //截取真正的token,去掉前缀"login:token:" 41 | if (token.startsWith("login:token:")) 42 | token = token.substring(12); 43 | List videoVOList = videoService.getPublishListByAuthorId(token, user_id); 44 | if (videoVOList == null) 45 | return VideoListVO.fail("用户没有作品"); 46 | return VideoListVO.success("成功", videoVOList); 47 | } 48 | 49 | /** 50 | * @author zzzi 51 | * @date 2024/3/29 12:14 52 | * 30个视频刷完按照这个当前推荐视频的最早时间继续推荐 53 | *

54 | * 没传latest_time,默认为最新时间 55 | * 推荐视频时,先拉取自己关注的大V的作品,然后拉取自己的收件箱中的视频 56 | * 二者结合得到推荐流 57 | * 1. 传递了token,先获取大V(拉模式),然后获取自己的关注(推模式) 58 | * 2. 没有传递token,先获取大V(拉模式),然后获取video数据集中最新的30个(推模式) 59 | */ 60 | @GetMapping("/feed") 61 | public VideoFeedListVO getFeedList(@RequestParam(required = false) Long latest_time, 62 | @RequestParam(required = false) String token) { 63 | log.info("视频推荐,token为 :{}", token); 64 | //截取真正的token,去掉前缀"login:token:" 65 | if (token != null && token.startsWith("login:token:")) 66 | token = token.substring(12); 67 | /**@author zzzi 68 | * @date 2024/4/2 16:50 69 | * 时间没传,默认从当前时间向前推荐 70 | */ 71 | if (latest_time == null) 72 | latest_time = System.currentTimeMillis(); 73 | //默认下次也从当前时间开始推荐,这样视频少的时候可以循环推荐 74 | //根据是否传递token调用不同的方法 75 | VideoFeedDTO videoFeedDTO = null; 76 | if ("".equals(token) || token == null) { 77 | videoFeedDTO = videoService.getFeedListWithOutToken(latest_time); 78 | } else { 79 | videoFeedDTO = videoService.getFeedListWithToken(latest_time, token); 80 | } 81 | //判断返回结果的形式 82 | if (videoFeedDTO != null) { 83 | List videoVOList = videoFeedDTO.getFeed_list(); 84 | 85 | //更新下次推荐时间 86 | Long next_time = videoFeedDTO.getNext_time(); 87 | return VideoFeedListVO.success("获取推荐视频成功", next_time, videoVOList); 88 | } 89 | return VideoFeedListVO.fail("获取推荐视频失败"); 90 | } 91 | 92 | /** 93 | * @author zzzi 94 | * @date 2024/3/27 14:44 95 | * 用户投稿视频 96 | * 可以根据用户的token解析出用户的userId 97 | */ 98 | @PostMapping("/publish/action") 99 | public CommonVO postVideo(MultipartFile data, String token, String title) { 100 | log.info("用户投稿,token 为:{}", token); 101 | //截取真正的token,去掉前缀"login:token:" 102 | if (token.startsWith("login:token:")) 103 | token = token.substring(12); 104 | videoService.postVideo(data, token, title); 105 | 106 | //只要不出错误,说明成功投稿 107 | return CommonVO.success("投稿成功"); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/dto/VideoFeedDTO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.dto; 2 | 3 | import com.zzzi.common.result.VideoVO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class VideoFeedDTO { 14 | 15 | private List feed_list; 16 | private Long next_time; 17 | } 18 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/entity/CommentDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Date; 13 | 14 | //视频评论实体类 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @TableName("comment") 19 | public class CommentDO extends Model { 20 | @TableId 21 | private Long commentId; 22 | private Long userId; 23 | private String commentText; 24 | private Long videoId; 25 | @TableField(fill = FieldFill.INSERT) 26 | private Date createTime; 27 | private Long parentId; 28 | private Long replyId; 29 | } 30 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/entity/FavoriteDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.entity; 2 | 3 | 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import com.baomidou.mybatisplus.extension.activerecord.Model; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | //用户关注实体类 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @TableName("favorite") 16 | public class FavoriteDO extends Model { 17 | @TableId 18 | private Long favoriteId; 19 | //谁点赞 20 | private Long userId; 21 | //哪个视频被点赞 22 | private Long videoId; 23 | } 24 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/entity/VideoDO.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.FieldFill; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.baomidou.mybatisplus.extension.activerecord.Model; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.Date; 13 | 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @TableName("video") 18 | public class VideoDO extends Model { 19 | @TableId 20 | private Long videoId; 21 | 22 | private Long authorId; 23 | 24 | private String coverUrl; 25 | 26 | private String playUrl; 27 | 28 | @TableField(fill = FieldFill.INSERT) 29 | private Date createTime; 30 | 31 | @TableField(fill = FieldFill.INSERT_UPDATE) 32 | private Date updateTime; 33 | 34 | @TableField(fill = FieldFill.INSERT) 35 | private String title; 36 | 37 | @TableField(fill = FieldFill.INSERT) 38 | private Integer favoriteCount; 39 | 40 | @TableField(fill = FieldFill.INSERT) 41 | private Integer commentCount; 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/interceptor/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.interceptor; 2 | 3 | import com.zzzi.common.exception.*; 4 | import com.zzzi.common.result.CommentActionVO; 5 | import com.zzzi.common.result.CommentListVO; 6 | import com.zzzi.common.result.CommonVO; 7 | import com.zzzi.common.result.VideoListVO; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import javax.security.auth.login.LoginException; 15 | 16 | 17 | /** 18 | * @author zzzi 19 | * @date 2024/3/26 22:34 20 | * 在这里处理videoservice中的所有异常 21 | */ 22 | @ControllerAdvice(annotations = {RestController.class}) 23 | @ResponseBody 24 | @Slf4j 25 | public class GlobalExceptionHandler { 26 | 27 | @ExceptionHandler(VideoException.class) 28 | public CommonVO VideoExceptionHandler(VideoException ex) { 29 | log.error(ex.getMessage()); 30 | 31 | if (ex.getMessage().contains("视频上传失败")) { 32 | return CommonVO.fail("视频上传失败"); 33 | } 34 | 35 | if (ex.getMessage().contains("视频保存失败")) { 36 | return CommonVO.fail("视频保存失败"); 37 | } 38 | if (ex.getMessage().contains("属性自动填充失败")) { 39 | return CommonVO.fail("属性自动填充失败"); 40 | } 41 | if (ex.getMessage().contains("视频已经存在")) { 42 | return CommonVO.fail("视频已经存在"); 43 | } 44 | 45 | if (ex.getMessage().contains("获取用户作品列表失败")) { 46 | return CommonVO.fail("获取用户作品列表失败"); 47 | } 48 | 49 | if (ex.getMessage().contains("当前用户未登录")) { 50 | return CommonVO.fail("当前用户未登录"); 51 | } 52 | if (ex.getMessage().contains("更新视频信息失败")) { 53 | return CommonVO.fail("更新视频信息失败"); 54 | } 55 | return CommonVO.fail("未知错误"); 56 | } 57 | 58 | @ExceptionHandler(UserException.class) 59 | public CommonVO UserExceptionHandler(UserException ex) { 60 | log.error(ex.getMessage()); 61 | 62 | if (ex.getMessage().contains("当前用户未登录")) { 63 | return CommonVO.fail("当前用户未登录"); 64 | } 65 | return CommonVO.fail("未知错误"); 66 | } 67 | 68 | @ExceptionHandler(LoginException.class) 69 | public CommonVO LoginExceptionHandler(LoginException ex) { 70 | log.error(ex.getMessage()); 71 | return CommonVO.fail("请先登录"); 72 | } 73 | 74 | @ExceptionHandler(Exception.class) 75 | public CommonVO CommonExceptionHandler(Exception ex) { 76 | log.error(ex.getMessage()); 77 | return CommonVO.fail("未知错误"); 78 | } 79 | 80 | @ExceptionHandler(VideoListException.class) 81 | public VideoListVO VideoListExceptionHandler(VideoListException ex) { 82 | log.error(ex.getMessage()); 83 | if (ex.getMessage().contains("当前用户未登录")) { 84 | return VideoListVO.fail("当前用户未登录"); 85 | } 86 | if (ex.getMessage().contains("获取用户作品列表失败")) { 87 | return VideoListVO.fail("获取用户作品列表失败"); 88 | } 89 | if (ex.getMessage().contains("获取用户喜欢列表失败")) { 90 | return VideoListVO.fail("获取用户喜欢列表失败"); 91 | } 92 | return VideoListVO.fail("未知错误"); 93 | } 94 | 95 | @ExceptionHandler(CommentActionException.class) 96 | public CommentActionVO CommentActionExceptionHandler(CommentActionException ex) { 97 | log.error(ex.getMessage()); 98 | if (ex.getMessage().contains("用户评论失败")) { 99 | return CommentActionVO.fail("用户评论失败"); 100 | } 101 | if (ex.getMessage().contains("用户删除评论失败")) { 102 | return CommentActionVO.fail("用户删除评论失败"); 103 | } 104 | return CommentActionVO.fail("未知错误"); 105 | } 106 | 107 | @ExceptionHandler(CommentListException.class) 108 | public CommentListVO CommentListExceptionHandler(CommentListException ex) { 109 | log.error(ex.getMessage()); 110 | if (ex.getMessage().contains("获取当前视频评论列表失败")) { 111 | return CommentListVO.fail("获取当前视频评论列表失败"); 112 | } 113 | return CommentListVO.fail("未知错误"); 114 | } 115 | 116 | @ExceptionHandler(RuntimeException.class) 117 | public CommonVO CommentListExceptionHandler(RuntimeException ex) { 118 | log.error(ex.getMessage()); 119 | if (ex.getMessage().contains("请勿重复点赞")) { 120 | return CommonVO.fail("请勿重复点赞"); 121 | } 122 | if (ex.getMessage().contains("用户点赞失败")) { 123 | return CommonVO.fail("用户点赞失败"); 124 | } 125 | return CommonVO.fail("未知错误"); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/interceptor/LoginUserInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.interceptor; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zzzi.common.constant.RedisKeys; 5 | import com.zzzi.common.result.CommonVO; 6 | import com.zzzi.common.utils.JwtUtils; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.AntPathMatcher; 14 | import org.springframework.web.servlet.HandlerInterceptor; 15 | 16 | import javax.security.auth.login.LoginException; 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | 20 | /** 21 | * @author zzzi 22 | * @date 2024/3/29 14:52 23 | * 不需要拦截的请求直接放行 24 | */ 25 | @Component 26 | @Slf4j 27 | public class LoginUserInterceptor implements HandlerInterceptor { 28 | @Autowired 29 | private StringRedisTemplate redisTemplate; 30 | 31 | //除了视频推流,其余的请求都要带上token,否则拦截 32 | @Override 33 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 34 | // 放行无需登录的请求 35 | String uri = request.getRequestURI(); 36 | AntPathMatcher antPathMatcher = new AntPathMatcher();// 匹配器 37 | boolean feed = antPathMatcher.match("/douyin/feed/**", uri);// 视频流 38 | log.info("登录拦截请求:" + uri); 39 | //放行无需登录的请求 40 | if (feed) { 41 | log.info("视频推流请求无需拦截"); 42 | return true; 43 | } 44 | 45 | // 验证登录状态 46 | /**@author zzzi 47 | * @date 2024/3/29 14:53 48 | * 直接根据缓存中是否存在用户的token来判断 49 | * 为了调试方便,先全部放行 50 | */ 51 | String token = request.getParameter("token"); 52 | //log.info("拦截到的请求中,token为:{}", token); 53 | ////截取得到真正的token 54 | //if (token != null && !"".equals(token)) { 55 | // token = token.substring(12); 56 | //} 57 | ////没有抛异常的话就是验签成功 58 | //Long userId = JwtUtils.getUserIdByToken(token); 59 | //String userToken = redisTemplate.opsForValue().get(RedisKeys.USER_TOKEN_PREFIX + userId); 60 | //if (userToken == null || "".equals(userToken)) { 61 | // log.error("用户未登录,非法请求"); 62 | // CommonVO fail = CommonVO.fail("请先登录"); 63 | // String failString = JSONObject.toJSONString(fail); 64 | // response.getWriter().write(failString); 65 | // return false; 66 | //} 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/CommentListener.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import ch.qos.logback.core.joran.conditional.ThenOrElseActionBase; 4 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 5 | import com.google.gson.Gson; 6 | import com.zzzi.common.constant.RabbitMQKeys; 7 | import com.zzzi.common.exception.CommentActionException; 8 | import com.zzzi.videoservice.entity.VideoDO; 9 | import com.zzzi.videoservice.mapper.VideoMapper; 10 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.amqp.core.ExchangeTypes; 13 | import org.springframework.amqp.rabbit.annotation.Exchange; 14 | import org.springframework.amqp.rabbit.annotation.Queue; 15 | import org.springframework.amqp.rabbit.annotation.QueueBinding; 16 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 17 | import org.springframework.aop.framework.AopContext; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.messaging.handler.annotation.Payload; 20 | import org.springframework.stereotype.Service; 21 | import org.springframework.transaction.annotation.Transactional; 22 | 23 | 24 | @Service 25 | @Slf4j 26 | public class CommentListener { 27 | 28 | @Autowired 29 | private VideoMapper videoMapper; 30 | @Autowired 31 | private UpdateVideoInfoUtils updateVideoInfoUtils; 32 | @Autowired 33 | private Gson gson; 34 | 35 | @RabbitListener( 36 | bindings = @QueueBinding( 37 | value = @Queue(name = "direct.comment"), 38 | exchange = @Exchange(name = RabbitMQKeys.COMMENT_EXCHANGE, type = ExchangeTypes.DIRECT), 39 | key = {RabbitMQKeys.COMMENT_KEY} 40 | ) 41 | ) 42 | @Transactional 43 | public void listenToComment(@Payload long videoId) { 44 | log.info("监听到用户评论操作"); 45 | 46 | //更新视频评论数 47 | VideoDO videoDO = videoMapper.selectById(videoId); 48 | Integer commentCount = videoDO.getCommentCount(); 49 | //加上乐观锁 50 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 51 | queryWrapper.eq(VideoDO::getCommentCount, commentCount); 52 | videoDO.setCommentCount(commentCount + 1); 53 | int update = videoMapper.update(videoDO, queryWrapper); 54 | //更新失败说明出现线程安全问题,此时评论失败 55 | if (update != 1) { 56 | CommentListener commentListener = (CommentListener) AopContext.currentProxy(); 57 | commentListener.listenToComment(videoId); 58 | } 59 | 60 | //更新视频缓存 61 | String videoDOJson = gson.toJson(videoDO); 62 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/FavoriteListenerOne.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.VideoException; 7 | import com.zzzi.videoservice.entity.VideoDO; 8 | import com.zzzi.videoservice.mapper.VideoMapper; 9 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.messaging.handler.annotation.Payload; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | import sun.awt.geom.AreaOp; 18 | 19 | 20 | /** 21 | * @author zzzi 22 | * @date 2024/3/29 16:38 23 | * 在这里异步的更新视频的基本信息 24 | */ 25 | @Service 26 | @Slf4j 27 | public class FavoriteListenerOne { 28 | @Autowired 29 | private VideoMapper videoMapper; 30 | @Autowired 31 | private UpdateVideoInfoUtils updateVideoInfoUtils; 32 | @Autowired 33 | private Gson gson; 34 | 35 | @RabbitListener(queues = {RabbitMQKeys.FAVORITE_VIDEO}) 36 | @Transactional 37 | public void listenToFavorite(@Payload long videoId) { 38 | log.info("第一个消费者监听到用户点赞操作"); 39 | 40 | //更新视频点赞数 41 | VideoDO videoDO = videoMapper.selectById(videoId); 42 | Integer favoriteCount = videoDO.getFavoriteCount(); 43 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 44 | //加上乐观锁 45 | queryWrapper.eq(VideoDO::getFavoriteCount, favoriteCount); 46 | videoDO.setFavoriteCount(favoriteCount + 1); 47 | int update = videoMapper.update(videoDO, queryWrapper); 48 | if (update != 1) { 49 | //手动实现CAS算法 50 | FavoriteListenerOne favoriteListener = (FavoriteListenerOne) AopContext.currentProxy(); 51 | favoriteListener.listenToFavorite(videoId); 52 | } 53 | 54 | //更新视频缓存 55 | String videoDOJson = gson.toJson(videoDO); 56 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/FavoriteListenerTwo.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.VideoException; 7 | import com.zzzi.videoservice.entity.VideoDO; 8 | import com.zzzi.videoservice.mapper.VideoMapper; 9 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.messaging.handler.annotation.Payload; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | 19 | /** 20 | * @author zzzi 21 | * @date 2024/3/29 16:38 22 | * 在这里异步的更新视频的基本信息 23 | */ 24 | @Service 25 | @Slf4j 26 | public class FavoriteListenerTwo { 27 | @Autowired 28 | private VideoMapper videoMapper; 29 | @Autowired 30 | private UpdateVideoInfoUtils updateVideoInfoUtils; 31 | @Autowired 32 | private Gson gson; 33 | 34 | @RabbitListener(queues = {RabbitMQKeys.FAVORITE_VIDEO}) 35 | @Transactional 36 | public void listenToFavorite(@Payload long videoId) { 37 | log.info("第二个消费者监听到用户点赞操作"); 38 | 39 | //更新视频点赞数 40 | VideoDO videoDO = videoMapper.selectById(videoId); 41 | Integer favoriteCount = videoDO.getFavoriteCount(); 42 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 43 | //加上乐观锁 44 | queryWrapper.eq(VideoDO::getFavoriteCount, favoriteCount); 45 | videoDO.setFavoriteCount(favoriteCount + 1); 46 | int update = videoMapper.update(videoDO, queryWrapper); 47 | if (update != 1) { 48 | //手动实现CAS算法 49 | FavoriteListenerTwo favoriteListener = (FavoriteListenerTwo) AopContext.currentProxy(); 50 | favoriteListener.listenToFavorite(videoId); 51 | } 52 | 53 | //更新视频缓存 54 | String videoDOJson = gson.toJson(videoDO); 55 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/UnCommentListener.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.CommentActionException; 7 | import com.zzzi.videoservice.entity.VideoDO; 8 | import com.zzzi.videoservice.mapper.VideoMapper; 9 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.core.ExchangeTypes; 12 | import org.springframework.amqp.rabbit.annotation.Exchange; 13 | import org.springframework.amqp.rabbit.annotation.Queue; 14 | import org.springframework.amqp.rabbit.annotation.QueueBinding; 15 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 16 | import org.springframework.aop.framework.AopContext; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.messaging.handler.annotation.Payload; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | 22 | 23 | @Service 24 | @Slf4j 25 | public class UnCommentListener { 26 | 27 | @Autowired 28 | private VideoMapper videoMapper; 29 | @Autowired 30 | private UpdateVideoInfoUtils updateVideoInfoUtils; 31 | @Autowired 32 | private Gson gson; 33 | 34 | @RabbitListener( 35 | bindings = @QueueBinding( 36 | value = @Queue(name = "direct.un_comment"), 37 | exchange = @Exchange(name = RabbitMQKeys.COMMENT_EXCHANGE, type = ExchangeTypes.DIRECT), 38 | key = {RabbitMQKeys.UN_COMMENT_KEY} 39 | ) 40 | ) 41 | @Transactional 42 | public void listenToUnComment(@Payload long videoId) { 43 | log.info("监听到用户删除评论操作"); 44 | 45 | //更新视频评论数数 46 | VideoDO videoDO = videoMapper.selectById(videoId); 47 | Integer commentCount = videoDO.getCommentCount(); 48 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 49 | //加上乐观锁 50 | queryWrapper.eq(VideoDO::getCommentCount, commentCount); 51 | videoDO.setCommentCount(commentCount - 1); 52 | int update = videoMapper.update(videoDO, queryWrapper); 53 | if (update != 1) { 54 | //手动实现CAS算法 55 | UnCommentListener unCommentListener = (UnCommentListener) AopContext.currentProxy(); 56 | unCommentListener.listenToUnComment(videoId); 57 | } 58 | 59 | //更新视频缓存 60 | String videoDOJson = gson.toJson(videoDO); 61 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/UnFavoriteListenerOne.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.VideoException; 7 | import com.zzzi.videoservice.entity.VideoDO; 8 | import com.zzzi.videoservice.mapper.VideoMapper; 9 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.messaging.handler.annotation.Payload; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | 19 | /** 20 | * @author zzzi 21 | * @date 2024/3/29 16:38 22 | * 在这里异步的更新用户的基本信息 23 | */ 24 | @Service 25 | @Slf4j 26 | public class UnFavoriteListenerOne { 27 | @Autowired 28 | private VideoMapper videoMapper; 29 | @Autowired 30 | private UpdateVideoInfoUtils updateVideoInfoUtils; 31 | @Autowired 32 | private Gson gson; 33 | 34 | 35 | /** 36 | * @author zzzi 37 | * @date 2024/4/2 13:24 38 | * 在这里更新对应的视频信息 39 | */ 40 | @RabbitListener(queues = {RabbitMQKeys.UN_FAVORITE_VIDEO}) 41 | @Transactional 42 | public void listenToUnFavorite(@Payload long videoId) { 43 | log.info("第一个消费者监听到用户取消点赞操作"); 44 | //更新视频点赞数 45 | VideoDO videoDO = videoMapper.selectById(videoId); 46 | Integer favoriteCount = videoDO.getFavoriteCount(); 47 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 48 | //加上乐观锁 49 | queryWrapper.eq(VideoDO::getFavoriteCount, favoriteCount); 50 | videoDO.setFavoriteCount(favoriteCount - 1); 51 | int update = videoMapper.update(videoDO, queryWrapper); 52 | if (update != 1) { 53 | //手动实现CAS算法 54 | UnFavoriteListenerOne unFavoriteListener = (UnFavoriteListenerOne) AopContext.currentProxy(); 55 | unFavoriteListener.listenToUnFavorite(videoId); 56 | } 57 | 58 | //更新视频缓存 59 | String videoDOJson = gson.toJson(videoDO); 60 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/listener/UnFavoriteListenerTwo.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.listener; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.google.gson.Gson; 5 | import com.zzzi.common.constant.RabbitMQKeys; 6 | import com.zzzi.common.exception.VideoException; 7 | import com.zzzi.videoservice.entity.VideoDO; 8 | import com.zzzi.videoservice.mapper.VideoMapper; 9 | import com.zzzi.common.utils.UpdateVideoInfoUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.aop.framework.AopContext; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.messaging.handler.annotation.Payload; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | 19 | /** 20 | * @author zzzi 21 | * @date 2024/3/29 16:38 22 | * 在这里异步的更新用户的基本信息 23 | */ 24 | @Service 25 | @Slf4j 26 | public class UnFavoriteListenerTwo { 27 | @Autowired 28 | private VideoMapper videoMapper; 29 | @Autowired 30 | private UpdateVideoInfoUtils updateVideoInfoUtils; 31 | @Autowired 32 | private Gson gson; 33 | 34 | /** 35 | * @author zzzi 36 | * @date 2024/4/2 13:24 37 | * 在这里更新对应的视频信息 38 | */ 39 | @RabbitListener(queues = {RabbitMQKeys.UN_FAVORITE_VIDEO}) 40 | @Transactional 41 | public void listenToUnFavorite(@Payload long videoId) { 42 | log.info("第二个消费者监听到用户取消点赞操作"); 43 | //更新视频点赞数 44 | VideoDO videoDO = videoMapper.selectById(videoId); 45 | Integer favoriteCount = videoDO.getFavoriteCount(); 46 | LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); 47 | //加上乐观锁 48 | queryWrapper.eq(VideoDO::getFavoriteCount, favoriteCount); 49 | videoDO.setFavoriteCount(favoriteCount - 1); 50 | int update = videoMapper.update(videoDO, queryWrapper); 51 | if (update != 1) { 52 | //手动实现CAS算法 53 | UnFavoriteListenerTwo unFavoriteListener = (UnFavoriteListenerTwo) AopContext.currentProxy(); 54 | unFavoriteListener.listenToUnFavorite(videoId); 55 | } 56 | 57 | //更新视频缓存 58 | String videoDOJson = gson.toJson(videoDO); 59 | updateVideoInfoUtils.updateVideoInfoCache(videoId, videoDOJson); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.videoservice.entity.CommentDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface CommentMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/mapper/FavoriteMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.videoservice.entity.FavoriteDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface FavoriteMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/mapper/VideoMapper.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.zzzi.videoservice.entity.VideoDO; 5 | import org.apache.ibatis.annotations.Mapper; 6 | 7 | @Mapper 8 | public interface VideoMapper extends BaseMapper { 9 | } 10 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.common.result.CommentVO; 5 | import com.zzzi.common.result.CommonVO; 6 | import com.zzzi.videoservice.entity.CommentDO; 7 | 8 | import java.util.List; 9 | 10 | public interface CommentService extends IService { 11 | List getCommentList(String token, String video_id); 12 | 13 | CommentVO commentParentAction(String token, String video_id, String comment_text, String parent_id,String reply_id); 14 | 15 | CommentVO commentParentUnAction(String token, String video_id, String comment_id); 16 | 17 | List getParentCommentList(String token, String parent_id); 18 | } 19 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/service/FavoriteService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.videoservice.entity.FavoriteDO; 5 | import com.zzzi.common.result.VideoVO; 6 | 7 | import java.util.List; 8 | 9 | public interface FavoriteService extends IService { 10 | void favoriteAction(String token, String video_id); 11 | 12 | void favoriteUnAction(String token, String video_id); 13 | 14 | List getFavoriteList(String user_id, String token); 15 | } 16 | -------------------------------------------------------------------------------- /video-service/src/main/java/com/zzzi/videoservice/service/VideoService.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.zzzi.common.result.UserVO; 5 | import com.zzzi.videoservice.dto.VideoFeedDTO; 6 | import com.zzzi.videoservice.entity.VideoDO; 7 | import com.zzzi.common.result.VideoVO; 8 | import org.springframework.web.multipart.MultipartFile; 9 | 10 | import java.util.List; 11 | 12 | 13 | public interface VideoService extends IService { 14 | void postVideo(MultipartFile data, String token, String title); 15 | 16 | List getPublishListByAuthorId(String token, Long user_id); 17 | 18 | //VideoFeedDTO getFeedList(Long latest_time, String token); 19 | 20 | VideoDO getVideoInfo(String videoId); 21 | 22 | VideoVO packageVideoVO(VideoDO videoDO, UserVO userVO, String user_id, String token); 23 | 24 | VideoVO packageFavoriteVideoVO(VideoDO videoDO, UserVO userVO); 25 | 26 | VideoFeedDTO getFeedListWithOutToken(Long latest_time); 27 | 28 | VideoFeedDTO getFeedListWithToken(Long latest_time, String token); 29 | } 30 | -------------------------------------------------------------------------------- /video-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.GREEN} 2 | ██╗ ██╗██╗██████╗ ███████╗ ██████╗ 3 | ██║ ██║██║██╔══██╗██╔════╝██╔═══██╗ 4 | ██║ ██║██║██║ ██║█████╗ ██║ ██║ 5 | ╚██╗ ██╔╝██║██║ ██║██╔══╝ ██║ ██║ 6 | ╚████╔╝ ██║██████╔╝███████╗╚██████╔╝ 7 | ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ 8 | ${AnsiColor.BRIGHT_BLACK} -------------------------------------------------------------------------------- /video-service/src/test/java/com/zzzi/videoservice/VideoServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.zzzi.videoservice; 2 | 3 | import com.zzzi.common.utils.JwtUtils; 4 | import com.zzzi.videoservice.entity.VideoDO; 5 | import com.zzzi.videoservice.mapper.VideoMapper; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 9 | import org.springframework.beans.factory.config.BeanPostProcessor; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | import java.text.SimpleDateFormat; 13 | import java.util.Date; 14 | 15 | @SpringBootTest 16 | class VideoServiceApplicationTests { 17 | @Autowired 18 | private VideoMapper videoMapper; 19 | 20 | @Test 21 | void contextLoads() { 22 | } 23 | 24 | @Test 25 | void testParseUserId() { 26 | String token = "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWKi5NUrJSconUDQ12DVLSUUqtKFCyMjQ3NDQzMLc0M9BRKi1OLfJMAYmZG1mYmxkYW1haGJuYmpgbGUEk_RJzU4FGGBoaOhQW6iXn5yrVAgBhI68YVwAAAA.tYVj46twIZzN1lJbbeelUqSt50_1zcS1Oujp9NL3WrUKYD7MSgYQE-CqdJiLnM5StVrBm-5dXfLotmyusGUjNg"; 27 | Long id = JwtUtils.getUserIdByToken(token); 28 | System.out.println(id); 29 | } 30 | 31 | @Test 32 | void testSubToken() { 33 | String token = "login:token:" + "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWKi5NUrJSconUDQ12DVLSUUqtKFCyMjQ3NDQzMLc0M9BRKi1OLfJMAYmZG1mYmxkYW1haGJuYmpgbGUEk_RJzU4FGGBoaOhQW6iXn5yrVAgBhI68YVwAAAA.tYVj46twIZzN1lJbbeelUqSt50_1zcS1Oujp9NL3WrUKYD7MSgYQE-CqdJiLnM5StVrBm-5dXfLotmyusGUjNg"; 34 | token = token.substring(12); 35 | System.out.println(token); 36 | } 37 | 38 | @Test 39 | void testParseDate() { 40 | VideoDO videoDO = videoMapper.selectById(1775447822198992898L); 41 | Date createTime = videoDO.getCreateTime(); 42 | SimpleDateFormat sdf = new SimpleDateFormat("MM-dd"); 43 | String create_date = sdf.format(createTime); 44 | System.out.println(create_date); 45 | } 46 | } 47 | --------------------------------------------------------------------------------