1. 访客功能

1.1 功能分析

在用户的首页,如果某一个用户查看过当前用户的详细信息,那么就会显示最近来访的访客。这一功能类似于QQ空间中的访客功能。

访客功能实际上分为了三大部分

  • 在用户查询某一个用户的详情页面的时,需要记录用户的访问记录
  • 访客查询功能:会自动的查询出最近来访的前五个用户,并显示在主页上
  • 用户点击访客,会显示完整的访客列表

1.2 保存访问记录

我们先来看第一部分的保存访客记录功能。这一个功能很简单,只需要修改原来查看用户详情的代码即可。我们先来看一下用户访问记录表的表结构。

需要注意的是,这里面有两个关于日期的字段。date记录的就是访问时间对应的时间戳,而visitDate记录的是访问的日期。同一个用户一天之内访问的记录我们只记录最新的一次,因此在代码中可以根据visitDate来判断当前用户是否今天已经访问。如果访问过则更新数据,否则插入数据。

代码实现具体如下:

/*** 查询佳人详情** @param userId 用户id* @return*/
public TodayBest getTodayBestById(Long userId) {// 1. 查询UserInfo表UserInfo userInfo = this.userInfoApi.getUserInfoById(userId);// 2. 查询RecommendUser表RecommendUser user = this.recommendUserApi.getRecommendUserByUserId(userId);// 记录访问记录Visitors visitors = new Visitors();visitors.setDate(System.currentTimeMillis());visitors.setVisitorUserId(UserHolder.getUserId());visitors.setUserId(userId);visitors.setVisitDate(new SimpleDateFormat("yyyyMMdd").format(new Date()));visitors.setScore(user.getScore());visitors.setFrom("圈子");this.visitorApi.save(visitors);// 3. 构建VOreturn TodayBest.init(userInfo, user);
}

1.3 查询最近访客

用户登陆后,可以在界面上看到自己最新的5个访客。这一点似乎直接从数据库中按照访问时间排序查询即可。但实际上,如果12点的时候有两个访客,用户点击了查询所有访客。那么13点的时候又有1个访客。如果仅仅按照访问时间排序查询的话,那么12点的访客也会被查询出来,但实际上用户点击查询所有访客后,那么12点的访客就不应该再出现了,而只显示13点的1个访客。

为了解决上述问题,我们可以将每一次用户点击了查询所有用户的时间戳保存起来,下一次查询只查询比保存的时间戳大的访客记录即可。在我们的项目中,我们把这个数据保存到了Redis中。Redis采用了hash的存储结构,其中每一个hashkey就是用户的id,里面存储的值就是时间戳。

这样查询最近访客功能的具体业务如下:

  • 首先根据用户id到Redis中查询最后查询时间。
  • 如果查询不到,那么直接从数据库中读取最新访问的5条记录即可
  • 如果查询得到,那么就需要在查询到时候在加上一个条件,即记录的时间要大于查询时间。

具体代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo implements Serializable {private Long userId;private String avatar; //头像private String nickname; //昵称private String id;private String cover; //封面private String videoUrl; //视频URLprivate String signature; //发布视频时,传入的文字内容private Integer likeCount; //点赞数量private Integer hasLiked; //是否已赞(1是,0否)private Integer hasFocus; //是否关注 (1是,0否)private Integer commentCount; //评论数量public static VideoVo init(UserInfo userInfo, Video item) {VideoVo vo = new VideoVo();//copy用户属性BeanUtils.copyProperties(userInfo,vo);  //source,target//copy视频属性BeanUtils.copyProperties(item,vo);vo.setCover(item.getPicUrl());vo.setId(item.getId().toHexString());vo.setSignature(item.getText());vo.setHasFocus(0);vo.setHasLiked(0);return vo;}
}
/*** 查询谁看过我** @return*/
@GetMapping("/visitors")
public ResponseEntity visitors() {List<VisitorsVo> voList = this.momentService.visitors();return ResponseEntity.ok(voList);
}
/*** 查询谁看过我** @return*/
public List<VisitorsVo> visitors() {// 1. 从Redis中获取数据 查看最后一次查看完整访客列表的时间// Redis中的key VISITORS 哈希key 用户id value为最后一次查看的时间戳String lastTime = (String) this.redisTemplate.opsForHash().get(VISITORS, UserHolder.getUserId().toString());Long time = lastTime != null ? Long.valueOf(lastTime) : null;// 2. 查询访客表List<Visitors> visitorsList = this.visitorApi.getVisitors(UserHolder.getUserId(), time);List<Long> ids = CollUtil.getFieldValues(visitorsList, "visitorUserId", Long.class);// 3. 查询访客的用户详情Map<Long, UserInfo> map = this.userInfoApi.getUserInfoByIds(ids, null);// 4. 封装数据List<VisitorsVo> voList = new ArrayList<>();for (Visitors visitors : visitorsList) {Long visitorUserId = visitors.getVisitorUserId();UserInfo userInfo = map.get(visitorUserId);if (userInfo != null) {voList.add(VisitorsVo.init(userInfo, visitors));}}return voList;
}
/*** 查询访客** @param userId* @param time* @return*/
@Override
public List<Visitors> getVisitors(Long userId, Long time) {Criteria criteria = Criteria.where("userId").is(userId);if (time != null) {criteria.and("date").gt(time);}Query query = new Query(criteria).limit(5).with(Sort.by(Sort.Order.desc("date")));return this.mongoTemplate.find(query, Visitors.class);
}

1.4 查询访客列表

这一个接口就非常好实现了,就是分页从数据库中查询访问记录,按照时间倒序排列即可。这里就不在赘述了。

2. FastDFS

2.1 使用背景

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。

小视频需要解决的一个问题是视频的存放问题。我们知道比起图片数据,视频占用的存储空间是非常大的,如果我们和图片一样利用第三方OSS来做的话,费用是非常恐怖的。因此在保存视频的时候,我们项目采用了FastDFS的分布式文件系统实现。

2.2 FastDFS 简介

FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。每个 tracker 节点地位平等。收集 Storage 集群的状态。

Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

FastDFS的上传流程如下:

  • Storage server 定时向tracker汇报自己的情况
  • 客户端发来向tracker发送文件上传请求
  • tracker查询可用的Storage server,并且将Storage server的IP和端口号返回给客户端
  • 客户端根据IP和端口号向Storage server上传文件
  • 文件上传完成后,Storage server返回文件的file_id,然后经过客户端拼接,最终形成可以访问的URL地址。

2.3 在Springboot中使用FastDFS

找到tanhua-server的pom文件,打开fastdfs的依赖如下

<dependency><groupId>com.github.tobato</groupId><artifactId>fastdfs-client</artifactId><version>1.26.7</version><exclusions><exclusion><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId></exclusion></exclusions>
</dependency>
application.yml
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs:so-timeout: 1500connect-timeout: 600#缩略图生成参数thumb-image:width: 150height: 150#TrackerList参数,支持多个tracker-list: 192.168.136.160:22122web-server-url: http://192.168.136.160:8888/
编写测试代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TanhuaServerApplication.class)
public class TestFastDFS {//测试将文件上传到FastDFS文件系统中//从调度服务器获取,一个目标存储服务器,上传@Autowiredprivate FastFileStorageClient client;@Autowiredprivate FdfsWebServer webServer;// 获取存储服务器的请求URL@Testpublic void testFileUpdate() throws FileNotFoundException {//1、指定文件File file = new File("D:\\1.jpg");//2、文件上传StorePath path = client.uploadFile(new FileInputStream(file),file.length(), "jpg", null);//3、拼接访问路径String url = webServer.getWebServerUrl() + path.getFullPath();}
}

4. 小视频功能

我们采用之前介绍的FastDFS来保存上传的小视频。小视频的表结构如下:

这里由于已经做过动态的点赞,评论和喜欢了,小视频和动态的这几个功能实现起来是一样的,因此这里重点是小视频的发布。

4.1 发布视频

发布视频的业务逻辑如下:

  • 将用户上传的视频保存到FastDFS中
  • 将视频封面保存到阿里云OSS中。
  • 创建Video对象,保存到数据库中。

具体代码如下:

/*** 发布小视频** @param videoThumbnail 视频封面* @param videoFile      视频文件* @return*/
@PostMapping
public ResponseEntity publish(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {this.videoService.save(videoThumbnail, videoFile);return ResponseEntity.ok(null);
}
/*** 发布小视频** @param videoThumbnail* @param videoFile* @throws IOException*/
public void save(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {if (videoFile.isEmpty() || videoThumbnail.isEmpty()) {// 文件为空throw new BusinessException(ErrorResult.error());}// 将缩略图保存到阿里云String imageUrl = this.ossTemplate.uploadFile(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());// 保存视频到Fast DFSString ext = videoFile.getOriginalFilename().substring(videoFile.getOriginalFilename().lastIndexOf(".") + 1);StorePath storePath = fastFileStorageClient.uploadFile(videoFile.getInputStream(), videoFile.getSize(), ext, null);String videoUrl = fdfsWebServer.getWebServerUrl() + storePath.getFullPath();// 创建Video对象Video video = new Video();video.setVideoUrl(videoUrl);video.setPicUrl(imageUrl);video.setUserId(UserHolder.getUserId());video.setText("这是我的第一条视频");String id = this.videoApi.save(video);if (StringUtils.isEmpty(id)) {throw new BusinessException(ErrorResult.error());}
}
@Override
public String save(Video video) {video.setVid(idWorker.getNextId("video"));video.setCreated(System.currentTimeMillis());return this.mongoTemplate.save(video).getId().toHexString();
}

在测试过程中,可能会出现文件大小超出限制的错误,我们只需要在配置文件中配置文件上传的大小限制即可。

Spring:servlet:multipart:max-file-size: 30MBmax-request-size: 30MB

4.2 查询视频列表

在后台会根据用户的喜好,给用户推荐相关的视频,我们的任务就是展示推荐给用户的视频。具体的业务逻辑如下:

  • 首先从Redis中查询推荐的视频vid
  • 如果查询不到,则直接查询小视频表,按照时间倒序排列
  • 根据查询到的vid,查询到相关的视频。
  • 由于还需要显示用户头像等信息,因此还需要根据视频作者的用户id查询到用户详细信息。
  • 最终封装VO返回

需要注意的是,在进行分页时。会存在一下的问题。假设目前Redis中有推荐的视频,用户进行分页查询,查询到第3页的时候,Redis中所有推荐的视频都读取完毕了,此时需要从数据库中分页查询。此时前端传递的页号为3,但是从数据库中肯定要从第一页查询。因此需要用户传递的page - Redis中所有推荐视频所占据的页数=数据库的页号

具体代码如下:

/*** 获取小视频列表** @param page* @param pagesize* @return*/
@GetMapping
public ResponseEntity getVideos(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "5") Integer pagesize) {PageResult pageResult = this.videoService.getVideos(page, pagesize);return ResponseEntity.ok(pageResult);
}
/*** 分页查询小视频列表** @param page* @param pagesize* @return*/
public PageResult getVideos(Integer page, Integer pagesize) {// 1. 获取用户idLong userId = UserHolder.getUserId();// 2. 根据用户Id 到redis中查询数据 判断推荐信息是否存在String key = Constants.VIDEOS_RECOMMEND + userId;String recommendStr = redisTemplate.opsForValue().get(key);List<Video> videoList = new ArrayList<>();int redisTotalPage = 0;if (StringUtils.isNotEmpty(recommendStr)) {String[] split = recommendStr.split(",");// 判断是否还需要分页if ((page - 1) * pagesize < split.length) {List<Long> vids = Arrays.stream(split).skip((page - 1) * pagesize).limit(pagesize).map(e -> Long.valueOf(e)).collect(Collectors.toList());// 根据pids查询出所有的movementvideoList = this.videoApi.getVideoByPids(vids);}redisTotalPage = PageUtil.totalPage(split.length, pagesize);}if (videoList.isEmpty()) {// 到MongoDB中按照时间顺序分页查找videoList = this.videoApi.getVideos(page - redisTotalPage, pagesize);}// 查询视频作者的详细信息List<Long> ids = CollUtil.getFieldValues(videoList, "userId", Long.class);Map<Long, UserInfo> map = this.userInfoApi.getUserInfoByIds(ids, null);// 封装对象List<VideoVo> voList = new ArrayList<>();for (Video video : videoList) {Long id = video.getUserId();UserInfo userInfo = map.get(id);if (userInfo != null) {voList.add(VideoVo.init(userInfo, video));}}return new PageResult(page, pagesize, 0, voList);
}

4.3 关注/取消关注视频作者

这一部分表简单,只涉及到对于user_focus表的操作。具体的表结构如下:

接口文档如下:

具体代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "focus_user")
public class FocusUser implements java.io.Serializable{private static final long serialVersionUID = 3148619072405056052L;private ObjectId id; //主键idprivate Long userId; //用户id    106private Long followUserId; //关注的用户id   1private Long created; //关注时间
}
/*** 关注小视频作者* @param userId* @return*/
@PostMapping("/{id}/userFocus")
public ResponseEntity focus(@PathVariable(name = "id") Long userId) {this.videoService.focus(userId);return ResponseEntity.ok(null);
}/*** 取消关注小视频作者* @param userId* @return*/
@PostMapping("/{id}/userUnFocus")
public ResponseEntity unfocus(@PathVariable(name = "id") Long userId) {this.videoService.unfocus(userId);return ResponseEntity.ok(null);
}
/*** 关注小视频作者** @param userId*/
public void focus(Long userId) {FocusUser focusUser = new FocusUser();focusUser.setUserId(UserHolder.getUserId());focusUser.setFollowUserId(userId);focusUser.setCreated(System.currentTimeMillis());this.focusUserApi.save(focusUser);// 保存到RedisString key = Constants.FOCUS_USER + UserHolder.getUserId();String hashKey = String.valueOf(userId);this.redisTemplate.opsForHash().put(key, hashKey, "1");
}/*** 取消关注小视频作者** @param userId*/
public void unfocus(Long userId) {FocusUser focusUser = new FocusUser();focusUser.setUserId(UserHolder.getUserId());focusUser.setFollowUserId(userId);this.focusUserApi.delete(focusUser);// 保存到RedisString key = Constants.FOCUS_USER + UserHolder.getUserId();String hashKey = String.valueOf(userId);this.redisTemplate.opsForHash().delete(key, hashKey);
}

需要注意的是,我们在保存用户关注的时候,也将用户关注的作者id保存到了Redis中,方便查询

4.4 Spring Cache改造

在小视频的查询中,可能查询的次数会比较频繁,因此应该引入缓存来降低数据库的压力。这里采用Spring Cache的方式对原有代码进行改造。

在发布视频的方法上添加一下内容:

@CacheEvict(value="video",allEntries = true)

在查询视频列表的方法上添加一下内容:

@Cacheable(value = "video",key = "T(com.tanhua.server.interceptor.UserHolder).getUserId() + '_' + #page +'_' + #pagesize"
)

具体有关Spring Cache的讲解看这一篇文章:https://blog.csdn.net/lyx7762/article/details/128486041

【探花交友DAY 09】最近访客和FastDFS实现小视频功能相关推荐

  1. 探花交友_第1章_项目介绍以及实现登录功能_第1节_功能介绍

    探花交友_第1章_项目介绍以及实现登录功能_第1节_功能介绍 文章目录 探花交友_第1章_项目介绍以及实现登录功能_第1节_功能介绍 1.功能介绍 1.1.功能列表 1.2.注册登录 1.3.交友 1 ...

  2. 探花交友_第6章_完善小视频功能以及即时通讯

    探花交友_第6章_完善小视频功能以及即时通讯 文章目录 探花交友_第6章_完善小视频功能以及即时通讯 1.视频点赞 1.1.dubbo服务 1.2.APP接口服务 1.2.1.VideoControl ...

  3. 探花交友_第1章_项目介绍以及实现登录功能_第2节_项目介绍

    探花交友_第1章_项目介绍以及实现登录功能_第2节_项目介绍 文章目录 探花交友_第1章_项目介绍以及实现登录功能_第2节_项目介绍 2.项目介绍 2.1.项目背景 2.2.市场分析 2.3.目标用户 ...

  4. 探花交友_第5章_圈子、小视频功能实现

    探花交友_第5章_圈子.小视频功能实现 文章目录 探花交友_第5章_圈子.小视频功能实现 1.圈子点赞实现分析 2.点赞 2.1.定义枚举 2.2.dubbo服务 2.2.1.定义接口 2.2.2.编 ...

  5. Dubbo+Flutter在线交友平台教程第六天 完善小视频功能以及即时通讯

    课程说明 实现视频点赞.评论.关注功能 了解什么是即时通信 了解探花交友的消息功能 了解即时通信的技术方案 了解环信的即时通讯 实现环信的用户体系集成 实现添加联系人.联系人列表功能 1.视频点赞 点 ...

  6. 探花交友_第9章_小视频方案(新版)

    探花交友_第9章_小视频方案(新版) 文章目录 探花交友_第9章_小视频方案(新版) 1. 我的访客 1.1 需求分析 1.1.1 功能说明 1.1.2 数据库表 1.2 记录访客数据 tanhua- ...

  7. 微信服务器是否记录视频信息,微信视频号有访客记录吗 微信视频号可以查看浏览记录吗...

    视频号是微信的一个全新版块,我们可以在视频号浏览视频号发布的短视频,同时还可以进行点赞.留言.转发等操作,那我们自行发布的视频,微信视频号有访客记录吗?下面就和IT百科一起来看看吧! 微信中的视频号, ...

  8. 人脸服务器如何与门禁系统对接,安全升级!人脸识别门禁终端集门禁考勤访客对讲一步到位...

    现在市面上很多人脸识别门禁系统,各种各样,如何选择呢?广东天波人脸识别门禁系统,功能齐全,包含:门禁.访客.考勤.音视频对讲等几个功能模块. 广东天波人脸识别门禁系统可以采用私有云部署,公有云部署,局 ...

  9. 个人网站性能优化经历(10)网站增加访客记录

    自己搭建了一个基于SpringBoot+Spring Security+MyBatis+MySQL+Redis+Thymeleaf的博客网站 上线个人云服务器后,发现服务器访问慢.个人服务器是1核2G ...

最新文章

  1. SpringBoot第十二篇:springboot集成apidoc
  2. XPath实例教程十四、following-sibling轴
  3. 12面魔方公式图解法_【高级篇】(三)三阶魔方CFOP高级玩法之——F2L
  4. 怎么关闭左下角的GL VERTS
  5. 为什么要接口管理软件???
  6. JS----深拷贝和浅拷贝
  7. 发送邮件时,如何附带上中文等价名信息
  8. 《Python Cookbook 3rd》笔记(2.17):在字符串中处理html和xml
  9. 【LOJ】#3123. 「CTS2019 | CTSC2019」重复
  10. MyCat分布式数据库集群架构工作笔记0002---数据库中间件对比
  11. 圆检测——最小二乘法拟合圆的推导
  12. Android 中AlarmManager升级4.2
  13. 2022届秋招的总结与体会
  14. leetcode 39 : 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
  15. 传奇架设好后,在登录游戏账号界面黑屏,并且中间有个小砖块,是什么情况?
  16. 什么是SQL注入攻击?
  17. excel之工作表工作簿保护暴力撤销
  18. ARM SIMD NEON 简介 (翻译自 Introducing NEON Development Article)
  19. 【BZOJ】2277: [Poi2011]Strongbox
  20. 安装Ubuntu遇到的问题及解决方案

热门文章

  1. t-sql的楼梯:超越基本级别6:使用案例表达式和IIF函数
  2. Stata:获取分组回归系数的三种方式
  3. c语言 rsa算法 分段,python3 实现RSA算法分段加密解密
  4. 对话 | 港科大教授权龙:为什么三维重建才是计算机视觉的灵魂?
  5. GPU cudaMallocManaged 统一内存的优劣点
  6. Android调用uc播放器,android 视频播放 -- 调用系统播放器
  7. 李保滨矩阵分析大作业2022:LU、QR、URV分解、Householder、Givens变换的程序实现
  8. 抓INOVANCE 汇川屏与汇川PLC通信
  9. 国产CPLD中AG1280Q48进行开发的实践之一:思路分析
  10. 推动遗留系统现代化革新的三种方式