课程说明

  • 圈子实现点赞、喜欢功能
  • 圈子实现评论
  • 圈子实现评论的点赞
  • 小视频功能介绍
  • FastDFS入门学习
  • 实现发布小视频功能
  • 实现查询小视频列表功能

    1、圈子点赞实现分析

    在圈子功能中,对于圈子的点赞、喜欢、评论等均可理解为用户对动态的评论(Comment),在quanzi_comment表中使用commentType进行区分。
    在具体的实现中,需要将点赞数、某用户是否点赞等数据保存到Reds中,以减轻MongoDB的压力。
    具体存储结构如下:
    day05-圈子、小视频功能实现 - 图1

    说明:在Redis的存储结构中,采用的是Hash存储,这样的好处就在于一条动态的点赞、喜欢等数据都会集中的存储到一起,从而减少了Redis中数据条数。

2、点赞

2.1、定义枚举

为了规范使用CommentType,所以将其定义为枚举类型。

  1. package com.tanhua.dubbo.server.enums;
  2. /**
  3. * 评论类型:1-点赞,2-评论,3-喜欢
  4. */
  5. public enum CommentType {
  6. LIKE(1), COMMENT(2), LOVE(3);
  7. int type;
  8. CommentType(int type) {
  9. this.type = type;
  10. }
  11. public int getType() {
  12. return type;
  13. }
  14. }

2.2、dubbo服务

2.2.1、定义接口

  1. package com.tanhua.dubbo.server.api;
  2. import com.tanhua.dubbo.server.pojo.Publish;
  3. import com.tanhua.dubbo.server.vo.PageInfo;
  4. public interface QuanZiApi {
  5. //........此处忽略其他代码..........
  6. /**
  7. * 根据id查询动态
  8. *
  9. * @param id 动态id
  10. * @return
  11. */
  12. Publish queryPublishById(String id);
  13. /**
  14. * 点赞
  15. *
  16. * @param userId
  17. * @param publishId
  18. * @return
  19. */
  20. Boolean likeComment(Long userId, String publishId);
  21. /**
  22. * 取消点赞
  23. *
  24. * @param userId
  25. * @param publishId
  26. * @return
  27. */
  28. Boolean disLikeComment(Long userId, String publishId);
  29. /**
  30. * 查询点赞数
  31. *
  32. * @param publishId
  33. * @return
  34. */
  35. Long queryLikeCount(String publishId);
  36. /**
  37. * 查询用户是否点赞该动态
  38. *
  39. * @param userId
  40. * @param publishId
  41. * @return
  42. */
  43. Boolean queryUserIsLike(Long userId, String publishId);
  44. }

2.2.2、编写实现

  1. package com.tanhua.dubbo.server.api;
  2. @Service(version = "1.0.0")
  3. @Slf4j
  4. public class QuanZiApiImpl implements QuanZiApi {
  5. //评论数据存储在Redis中key的前缀
  6. private static final String COMMENT_REDIS_KEY_PREFIX = "QUANZI_COMMENT_";
  7. //用户是否点赞的前缀
  8. private static final String COMMENT_USER_LIEK_REDIS_KEY_PREFIX = "USER_LIKE_";
  9. //用户是否喜欢的前缀
  10. private static final String COMMENT_USER_LOVE_REDIS_KEY_PREFIX = "USER_LOVE_";
  11. @Autowired
  12. private MongoTemplate mongoTemplate;
  13. @Autowired
  14. private RedisTemplate<String, String> redisTemplate;
  15. //........此处忽略其他代码..........
  16. @Override
  17. public Publish queryPublishById(String id) {
  18. return this.mongoTemplate.findById(new ObjectId(id), Publish.class);
  19. }
  20. @Override
  21. public Boolean likeComment(Long userId, String publishId) {
  22. //判断该用户是否已经点赞,如果已经点赞就直接返回
  23. if (this.queryUserIsLike(userId, publishId)) {
  24. return false;
  25. }
  26. //保存Comment数据
  27. Boolean result = this.saveComment(userId, publishId, CommentType.LIKE, null);
  28. if (!result) {
  29. return false;
  30. }
  31. //修改redis中的点赞数以及是否点赞
  32. //修改点赞数
  33. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  34. String hashKey = CommentType.LIKE.toString();
  35. this.redisTemplate.opsForHash().increment(redisKey, hashKey, 1);
  36. //用户是否点赞
  37. String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
  38. this.redisTemplate.opsForHash().put(redisKey, userHashKey, "1");
  39. return true;
  40. }
  41. private String getCommentRedisKeyPrefix(String publishId) {
  42. return COMMENT_REDIS_KEY_PREFIX + publishId;
  43. }
  44. private String getCommentUserLikeRedisKeyPrefix(Long userId) {
  45. return COMMENT_USER_LIKE_REDIS_KEY_PREFIX + userId;
  46. }
  47. @Override
  48. public Boolean disLikeComment(Long userId, String publishId) {
  49. //判断用户是否已经点赞,如果没有点赞就返回
  50. if (!this.queryUserIsLike(userId, publishId)) {
  51. return false;
  52. }
  53. //删除评论数据
  54. Boolean result = this.removeComment(userId, publishId, CommentType.LIKE);
  55. if (!result) {
  56. return false;
  57. }
  58. //修改Redis中的数据
  59. //修改点赞数
  60. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  61. String hashKey = CommentType.LIKE.toString();
  62. this.redisTemplate.opsForHash().increment(redisKey, hashKey, -1);
  63. //用户是否点赞
  64. String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
  65. this.redisTemplate.opsForHash().delete(redisKey, userHashKey);
  66. return true;
  67. }
  68. @Override
  69. public Long queryLikeCount(String publishId) {
  70. //从Redis中命中查询,如果命中直接返回即可
  71. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  72. String hashKey = CommentType.LIKE.toString();
  73. Object data = this.redisTemplate.opsForHash().get(redisKey, hashKey);
  74. if (ObjectUtil.isNotEmpty(data)) {
  75. return Convert.toLong(data);
  76. }
  77. //查询Mongodb
  78. Long count = this.queryCommentCount(publishId, CommentType.LIKE);
  79. //写入Redis中
  80. this.redisTemplate.opsForHash().put(redisKey, hashKey, String.valueOf(count));
  81. return count;
  82. }
  83. @Override
  84. public Boolean queryUserIsLike(Long userId, String publishId) {
  85. //从redis中查询数据
  86. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  87. String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
  88. Object data = this.redisTemplate.opsForHash().get(redisKey, userHashKey);
  89. if (ObjectUtil.isNotEmpty(data)) {
  90. return StrUtil.equals(Convert.toStr(data), "1");
  91. }
  92. //查询Mongodb,确定是否已经点赞
  93. Query query = Query.query(Criteria.where("publishId").is(new ObjectId(publishId))
  94. .and("userId").is(userId)
  95. .and("commentType").is(CommentType.LIKE)
  96. );
  97. long count = this.mongoTemplate.count(query, Comment.class);
  98. if(count == 0){
  99. return false;
  100. }
  101. //写入到redis中
  102. this.redisTemplate.opsForHash().put(redisKey, userHashKey, "1");
  103. return true;
  104. }
  105. /**
  106. * 保存Comment
  107. *
  108. * @return
  109. */
  110. private Boolean saveComment(Long userId, String publishId,
  111. CommentType commentType, String content) {
  112. try {
  113. Comment comment = new Comment();
  114. comment.setId(ObjectId.get());
  115. comment.setUserId(userId);
  116. comment.setPublishId(new ObjectId(publishId));
  117. // 评论类型
  118. comment.setCommentType(commentType.getType());
  119. // 内容
  120. comment.setContent(content);
  121. comment.setCreated(System.currentTimeMillis());
  122. Publish publish = this.queryPublishById(publishId);
  123. //TODO 其他评论对象,暂不处理
  124. comment.setPublishUserId(publish.getUserId());
  125. this.mongoTemplate.save(comment);
  126. return true;
  127. } catch (Exception e) {
  128. log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e);
  129. }
  130. return false;
  131. }
  132. /**
  133. * 删除评论数据
  134. *
  135. * @param userId
  136. * @param publishId
  137. * @param commentType
  138. * @return
  139. */
  140. private Boolean removeComment(Long userId, String publishId, CommentType commentType) {
  141. Query query = Query.query(Criteria.where("userId").is(userId)
  142. .and("publishId").is(new ObjectId(publishId))
  143. .and("commentType").is(commentType.getType())
  144. );
  145. return this.mongoTemplate.remove(query, Comment.class).getDeletedCount() > 0;
  146. }
  147. /**
  148. * 查询数量
  149. *
  150. * @param publishId
  151. * @param commentType
  152. * @return
  153. */
  154. private Long queryCommentCount(String publishId, CommentType commentType) {
  155. Query query = Query.query(Criteria.where("publishId").is(new ObjectId(publishId))
  156. .and("commentType").is(commentType.getType())
  157. );
  158. return this.mongoTemplate.count(query, Comment.class);
  159. }
  160. }

2.2.3、编写测试用例

  1. package com.tanhua.dubbo.server.api;
  2. import com.tanhua.dubbo.server.pojo.Publish;
  3. import com.tanhua.dubbo.server.vo.PageInfo;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.boot.test.context.SpringBootTest;
  8. import org.springframework.test.context.junit4.SpringRunner;
  9. @RunWith(SpringRunner.class)
  10. @SpringBootTest
  11. public class TestQuanZiApi {
  12. @Autowired
  13. private QuanZiApi quanZiApi;
  14. //........此处忽略其他代码..........
  15. @Test
  16. public void testLike(){
  17. Long userId = 1L;
  18. String publishId = "5fae53947e52992e78a3afb1";
  19. Boolean data = this.quanZiApi.queryUserIsLike(userId, publishId);
  20. System.out.println(data);
  21. System.out.println(this.quanZiApi.likeComment(userId, publishId));
  22. System.out.println(this.quanZiApi.queryLikeCount(publishId));
  23. System.out.println(this.quanZiApi.disLikeComment(userId, publishId));
  24. System.out.println(this.quanZiApi.queryLikeCount(publishId));
  25. }
  26. }

2.3、APP接口服务

点赞接口地址:https://mock-java.itheima.net/project/35/interface/api/707
day05-圈子、小视频功能实现 - 图2
day05-圈子、小视频功能实现 - 图3
从接口文档来看,点赞完成后需要返回点赞数。

2.3.1、编写接口服务

  1. //com.tanhua.server.controller.QuanZiController
  2. /**
  3. * 点赞
  4. *
  5. * @param publishId
  6. * @return
  7. */
  8. @GetMapping("/{id}/like")
  9. public ResponseEntity<Long> likeComment(@PathVariable("id") String publishId) {
  10. try {
  11. Long likeCount = this.quanZiService.likeComment(publishId);
  12. if (likeCount != null) {
  13. return ResponseEntity.ok(likeCount);
  14. }
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  19. }
  20. /**
  21. * 取消点赞
  22. *
  23. * @param publishId
  24. * @return
  25. */
  26. @GetMapping("/{id}/dislike")
  27. public ResponseEntity<Long> disLikeComment(@PathVariable("id") String publishId) {
  28. try {
  29. Long likeCount = this.quanZiService.disLikeComment(publishId);
  30. if (null != likeCount) {
  31. return ResponseEntity.ok(likeCount);
  32. }
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  37. }

2.3.2、编写服务实现

  1. //com.tanhua.server.service.QuanZiService
  2. public Long likeComment(String publishId) {
  3. User user = UserThreadLocal.get();
  4. //点赞
  5. Boolean result = this.quanZiApi.likeComment(user.getId(), publishId);
  6. if(result){
  7. //查询点赞数
  8. return this.quanZiApi.queryLikeCount(publishId);
  9. }
  10. return null;
  11. }
  12. public Long disLikeComment(String publishId) {
  13. User user = UserThreadLocal.get();
  14. //取消点赞
  15. Boolean result = this.quanZiApi.disLikeComment(user.getId(), publishId);
  16. if(result){
  17. //查询点赞数
  18. return this.quanZiApi.queryLikeCount(publishId);
  19. }
  20. return null;
  21. }

2.3.3、修改查询动态点赞数

查询点赞数、是否点赞,需要通过dubbo服务进行查询。

  1. //com.tanhua.server.service.QuanZiService
  2. /**
  3. * 填充用户信息
  4. *
  5. * @param userInfo
  6. * @param quanZiVo
  7. */
  8. private void fillUserInfoToQuanZiVo(UserInfo userInfo, QuanZiVo quanZiVo) {
  9. BeanUtil.copyProperties(userInfo, quanZiVo, "id");
  10. quanZiVo.setGender(userInfo.getSex().name().toLowerCase());
  11. quanZiVo.setTags(StringUtils.split(userInfo.getTags(), ','));
  12. //当前用户
  13. User user = UserThreadLocal.get();
  14. quanZiVo.setCommentCount(0); //TODO 评论数
  15. quanZiVo.setDistance("1.2公里"); //TODO 距离
  16. quanZiVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
  17. quanZiVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(quanZiVo.getId()))); //点赞数
  18. quanZiVo.setHasLoved(0); //TODO 是否喜欢(1是,0否)
  19. quanZiVo.setLoveCount(0); //TODO 喜欢数
  20. }

2.3.4、测试

day05-圈子、小视频功能实现 - 图4
从测试结果中可以看出,在响应结果中返回了点赞数以及是否点赞的数据。

3、喜欢

喜欢的实现与点赞类似,只是其类型不同。需要注意的是,在推荐动态中才有喜欢功能,好友动态中是没有此功能的。

3.1、dubbo服务

3.1.1、定义接口

  1. //com.tanhua.dubbo.server.api.QuanZiApi
  2. /**
  3. * 喜欢
  4. *
  5. * @param userId
  6. * @param publishId
  7. * @return
  8. */
  9. Boolean loveComment(Long userId, String publishId);
  10. /**
  11. * 取消喜欢
  12. *
  13. * @param userId
  14. * @param publishId
  15. * @return
  16. */
  17. Boolean disLoveComment(Long userId, String publishId);
  18. /**
  19. * 查询喜欢数
  20. *
  21. * @param publishId
  22. * @return
  23. */
  24. Long queryLoveCount(String publishId);
  25. /**
  26. * 查询用户是否喜欢该动态
  27. *
  28. * @param userId
  29. * @param publishId
  30. * @return
  31. */
  32. Boolean queryUserIsLove(Long userId, String publishId);

3.1.2、编写实现

  1. //com.tanhua.dubbo.server.api.QuanZiApiImpl
  2. @Override
  3. public Boolean loveComment(Long userId, String publishId) {
  4. //查询该用户是否已经喜欢
  5. if (this.queryUserIsLove(userId, publishId)) {
  6. return false;
  7. }
  8. //喜欢
  9. boolean result = this.saveComment(userId, publishId, CommentType.LOVE, null);
  10. if (!result) {
  11. return false;
  12. }
  13. //喜欢成功后,修改Redis中的总的喜欢数
  14. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  15. String hashKey = CommentType.LOVE.toString();
  16. this.redisTemplate.opsForHash().increment(redisKey, hashKey, 1);
  17. //标记用户已经喜欢
  18. hashKey = this.getCommentUserLoveRedisKey(userId);
  19. this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");
  20. return true;
  21. }
  22. private String getCommentUserLoveRedisKey(Long userId) {
  23. return COMMENT_USER_LOVE_REDIS_KEY_PREFIX + userId;
  24. }
  25. @Override
  26. public Boolean disLoveComment(Long userId, String publishId) {
  27. if (!this.queryUserIsLove(userId, publishId)) {
  28. //如果用户没有喜欢,就直接返回
  29. return false;
  30. }
  31. boolean result = this.removeComment(userId, publishId, CommentType.LOVE);
  32. if (!result) {
  33. //删除失败
  34. return false;
  35. }
  36. //删除redis中的记录
  37. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  38. String hashKey = this.getCommentUserLoveRedisKey(userId);
  39. this.redisTemplate.opsForHash().delete(redisKey, hashKey);
  40. this.redisTemplate.opsForHash().increment(redisKey, CommentType.LOVE.toString(), -1);
  41. return true;
  42. }
  43. @Override
  44. public Long queryLoveCount(String publishId) {
  45. //首先从redis中命中,如果命中的话就返回,没有命中就查询Mongodb
  46. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  47. String hashKey = CommentType.LOVE.toString();
  48. Object value = this.redisTemplate.opsForHash().get(redisKey, hashKey);
  49. if (ObjectUtil.isNotEmpty(value)) {
  50. return Convert.toLong(value);
  51. }
  52. //查询count
  53. Long count = this.queryCommentCount(publishId, CommentType.LOVE);
  54. //存储到redis中
  55. this.redisTemplate.opsForHash().put(redisKey, hashKey, String.valueOf(count));
  56. return count;
  57. }
  58. @Override
  59. public Boolean queryUserIsLove(Long userId, String publishId) {
  60. String redisKey = this.getCommentRedisKeyPrefix(publishId);
  61. String hashKey = this.getCommentUserLoveRedisKey(userId);
  62. Object value = this.redisTemplate.opsForHash().get(redisKey, hashKey);
  63. if (ObjectUtil.isNotEmpty(value)) {
  64. return StrUtil.equals(Convert.toStr(value), "1");
  65. }
  66. //查询mongodb
  67. Query query = Query.query(Criteria.where("publishId")
  68. .is(new ObjectId(publishId))
  69. .and("userId").is(userId)
  70. .and("commentType").is(CommentType.LOVE.getType()));
  71. long count = this.mongoTemplate.count(query, Comment.class);
  72. if (count == 0) {
  73. return false;
  74. }
  75. //标记用户已经喜欢
  76. this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");
  77. return true;
  78. }

3.2、APP接口服务

3.2.1、编写接口服务

  1. //com.tanhua.server.controller.QuanZiController
  2. /**
  3. * 喜欢
  4. *
  5. * @param publishId
  6. * @return
  7. */
  8. @GetMapping("/{id}/love")
  9. public ResponseEntity<Long> loveComment(@PathVariable("id") String publishId) {
  10. try {
  11. Long loveCount = this.quanZiService.loveComment(publishId);
  12. if (null != loveCount) {
  13. return ResponseEntity.ok(loveCount);
  14. }
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  19. }
  20. /**
  21. * 取消喜欢
  22. *
  23. * @param publishId
  24. * @return
  25. */
  26. @GetMapping("/{id}/unlove")
  27. public ResponseEntity<Long> disLoveComment(@PathVariable("id") String publishId) {
  28. try {
  29. Long loveCount = this.quanZiService.disLoveComment(publishId);
  30. if (null != loveCount) {
  31. return ResponseEntity.ok(loveCount);
  32. }
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  37. }

3.2.2、编写服务实现

  1. //com.tanhua.server.service.QuanZiService
  2. public Long loveComment(String publishId) {
  3. User user = UserThreadLocal.get();
  4. //喜欢
  5. Boolean result = this.quanZiApi.loveComment(user.getId(), publishId);
  6. if(result){
  7. //查询喜欢数
  8. return this.quanZiApi.queryLoveCount(publishId);
  9. }
  10. return null;
  11. }
  12. public Long disLoveComment(String publishId) {
  13. User user = UserThreadLocal.get();
  14. //取消喜欢
  15. Boolean result = this.quanZiApi.disLoveComment(user.getId(), publishId);
  16. if(result){
  17. //查询喜欢数
  18. return this.quanZiApi.queryLoveCount(publishId);
  19. }
  20. return null;
  21. }
  22. /**
  23. * 填充用户信息
  24. *
  25. * @param userInfo
  26. * @param quanZiVo
  27. */
  28. private void fillUserInfoToQuanZiVo(UserInfo userInfo, QuanZiVo quanZiVo) {
  29. BeanUtil.copyProperties(userInfo, quanZiVo, "id");
  30. quanZiVo.setGender(userInfo.getSex().name().toLowerCase());
  31. quanZiVo.setTags(StringUtils.split(userInfo.getTags(), ','));
  32. //当前用户
  33. User user = UserThreadLocal.get();
  34. quanZiVo.setCommentCount(0); //TODO 评论数
  35. quanZiVo.setDistance("1.2公里"); //TODO 距离
  36. quanZiVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
  37. quanZiVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(quanZiVo.getId()))); //点赞数
  38. quanZiVo.setHasLoved(this.quanZiApi.queryUserIsLove(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否喜欢(1是,0否)
  39. quanZiVo.setLoveCount(Convert.toInt(this.quanZiApi.queryLoveCount(quanZiVo.getId()))); //喜欢数
  40. }

1.2.3、测试

day05-圈子、小视频功能实现 - 图5

4、查询单条动态

用户点击评论时需要查询单条动态详情,需要有接口支持。
服务接口地址:https://mock-java.itheima.net/project/35/interface/api/695
day05-圈子、小视频功能实现 - 图6
响应的数据接口与查询好友动态一致,只是单条返回而不是集合。
要注意的是,dubbo服务接口在前面已经开发完成,现在只要想实现APP端的接口服务即可。

4.1、定义服务接口

  1. //com.tanhua.server.controller.QuanZiController
  2. /**
  3. * 查询单条动态信息
  4. *
  5. * @param publishId
  6. * @return
  7. */
  8. @GetMapping("/{id}")
  9. public ResponseEntity<QuanZiVo> queryById(@PathVariable("id") String publishId) {
  10. try {
  11. QuanZiVo movements = this.quanZiService.queryById(publishId);
  12. if(null != movements){
  13. return ResponseEntity.ok(movements);
  14. }
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  19. }

4.2、服务实现

  1. //com.tanhua.server.service.QuanZiService
  2. public QuanZiVo queryById(String publishId) {
  3. Publish publish = this.quanZiApi.queryPublishById(publishId);
  4. if (publish == null) {
  5. return null;
  6. }
  7. return this.fillQuanZiVo(Arrays.asList(publish)).get(0);
  8. }

4.3、测试

day05-圈子、小视频功能实现 - 图7
可以看到,返回了单条数据。

4.4、异常的解决

在完成查询单条动态接口后,会发现,刷新首页时会出现如下异常:

  1. java.lang.IllegalArgumentException: invalid hexadecimal representation of an ObjectId: [visitors]
  2. at org.bson.types.ObjectId.parseHexString(ObjectId.java:550)
  3. at org.bson.types.ObjectId.<init>(ObjectId.java:239)
  4. at com.tanhua.dubbo.server.api.QuanZiApiImpl.queryPublishById(QuanZiApiImpl.java:411)
  5. at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
  6. at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
  7. at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:76)
  8. at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:52)
  9. at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)

原因是:谁看过我的接口还没实现,导致了映射到了查询单条动态的接口,导致的异常,接口地址:https://mock-java.itheima.net/project/35/interface/api/743
解决方法:编写一个空的方法《谁看过我》的接口实现。

  1. //com.tanhua.server.controller.QuanZiController
  2. /**
  3. * TODO:谁看过我
  4. *
  5. * @return
  6. */
  7. @GetMapping("visitors")
  8. public ResponseEntity<Object> queryVisitors() {
  9. return ResponseEntity.ok(Collections.EMPTY_LIST);
  10. }

5、评论

在单条动态打开后,可以看到有评论列表,功能包括:查询评论列表,评论点赞、取消点赞。
需要注意的是,评论的点赞操作与圈子动态的点赞使用同一套逻辑。

5.1、dubbo服务

5.1.1、定义服务接口

  1. //com.tanhua.dubbo.server.api.QuanZiApi
  2. /**
  3. * 查询评论
  4. *
  5. * @return
  6. */
  7. PageInfo<Comment> queryCommentList(String publishId, Integer page, Integer pageSize);
  8. /**
  9. * 发表评论
  10. *
  11. * @param userId
  12. * @param publishId
  13. * @param content
  14. * @return
  15. */
  16. Boolean saveComment(Long userId, String publishId, String content);

5.1.2、编写实现

  1. /**
  2. * 查询评论列表
  3. *
  4. * @param publishId
  5. * @param page
  6. * @param pageSize
  7. * @return
  8. */
  9. @Override
  10. public PageInfo<Comment> queryCommentList(String publishId, Integer page, Integer pageSize) {
  11. PageRequest pageRequest = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.asc("created")));
  12. Query query = new Query(Criteria
  13. .where("publishId").is(new ObjectId(publishId))
  14. .and("commentType").is(CommentType.COMMENT.getType())).with(pageRequest);
  15. //查询评论列表
  16. List<Comment> commentList = this.mongoTemplate.find(query, Comment.class);
  17. PageInfo<Comment> pageInfo = new PageInfo<>();
  18. pageInfo.setPageNum(page);
  19. pageInfo.setPageSize(pageSize);
  20. pageInfo.setRecords(commentList);
  21. return pageInfo;
  22. }
  23. /**
  24. * 发表评论
  25. *
  26. * @param userId
  27. * @param publishId
  28. * @param content
  29. * @return
  30. */
  31. @Override
  32. public Boolean saveComment(Long userId, String publishId, String content) {
  33. return this.saveComment(userId, publishId, CommentType.COMMENT, content);
  34. }

5.2、APP接口服务

5.2.1、CommentVo

根据响应结果的数据结构定义对象:

  1. package com.tanhua.server.vo;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. /**
  6. * 评论
  7. */
  8. @Data
  9. @NoArgsConstructor
  10. @AllArgsConstructor
  11. public class CommentVo {
  12. private String id; //评论id
  13. private String avatar; //头像
  14. private String nickname; //昵称
  15. private String content; //评论
  16. private String createDate; //评论时间: 08:27
  17. private Integer likeCount; //点赞数
  18. private Integer hasLiked; //是否点赞(1是,0否)
  19. }

5.2.2、编写Controller

在APP接口服务中,需要开发4个接口,分别是查询评论列表、发表评论、点赞、取消点赞。
由于其接口的url地址与QuanZiConroller地址不同,所以需要创建不同的Controller类。

  1. package com.tanhua.server.controller;
  2. import com.tanhua.server.service.QuanZiService;
  3. import com.tanhua.server.vo.PageResult;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.http.HttpStatus;
  6. import org.springframework.http.ResponseEntity;
  7. import org.springframework.web.bind.annotation.*;
  8. import java.util.Map;
  9. /**
  10. * 圈子功能中的评论
  11. */
  12. @RestController
  13. @RequestMapping("comments")
  14. public class QuanZiCommentController {
  15. @Autowired
  16. private QuanZiService quanZiService;
  17. /**
  18. * 查询评论列表
  19. *
  20. * @return
  21. */
  22. @GetMapping
  23. public ResponseEntity<PageResult> queryCommentsList(@RequestParam("movementId") String publishId,
  24. @RequestParam(value = "page", defaultValue = "1") Integer page,
  25. @RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
  26. try {
  27. PageResult pageResult = this.quanZiService.queryCommentList(publishId, page, pageSize);
  28. return ResponseEntity.ok(pageResult);
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. }
  32. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  33. }
  34. /**
  35. * 保存评论
  36. */
  37. @PostMapping
  38. public ResponseEntity<Void> saveComments(@RequestBody Map<String, String> param) {
  39. try {
  40. String publishId = param.get("movementId");
  41. String content = param.get("comment");
  42. Boolean result = this.quanZiService.saveComments(publishId, content);
  43. if (result) {
  44. return ResponseEntity.ok(null);
  45. }
  46. } catch (Exception e) {
  47. e.printStackTrace();
  48. }
  49. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  50. }
  51. /**
  52. * 点赞
  53. *
  54. * @param publishId
  55. * @return
  56. */
  57. @GetMapping("{id}/like")
  58. public ResponseEntity<Long> likeComment(@PathVariable("id") String publishId) {
  59. try {
  60. Long likeCount = this.quanZiService.likeComment(publishId);
  61. if (likeCount != null) {
  62. return ResponseEntity.ok(likeCount);
  63. }
  64. } catch (Exception e) {
  65. e.printStackTrace();
  66. }
  67. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  68. }
  69. /**
  70. * 取消点赞
  71. *
  72. * @param publishId
  73. * @return
  74. */
  75. @GetMapping("{id}/dislike")
  76. public ResponseEntity<Long> disLikeComment(@PathVariable("id") String publishId) {
  77. try {
  78. Long likeCount = this.quanZiService.disLikeComment(publishId);
  79. if (null != likeCount) {
  80. return ResponseEntity.ok(likeCount);
  81. }
  82. } catch (Exception e) {
  83. e.printStackTrace();
  84. }
  85. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  86. }
  87. }

5.2.3、编写Service实现

Service的具体实现依然是放到QuanZiSerivce中完成。

  1. //com.tanhua.server.service.QuanZiService
  2. /**
  3. * 查询评论列表
  4. *
  5. * @param publishId
  6. * @param page
  7. * @param pageSize
  8. * @return
  9. */
  10. public PageResult queryCommentList(String publishId, Integer page, Integer pageSize) {
  11. PageResult pageResult = new PageResult();
  12. pageResult.setPage(page);
  13. pageResult.setPagesize(pageSize);
  14. User user = UserThreadLocal.get();
  15. //查询评论列表数据
  16. PageInfo<Comment> pageInfo = this.quanZiApi.queryCommentList(publishId, page, pageSize);
  17. List<Comment> records = pageInfo.getRecords();
  18. if(CollUtil.isEmpty(records)){
  19. return pageResult;
  20. }
  21. //查询用户信息
  22. List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
  23. List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);
  24. List<CommentVo> result = new ArrayList<>();
  25. for (Comment record : records) {
  26. CommentVo commentVo = new CommentVo();
  27. commentVo.setContent(record.getContent());
  28. commentVo.setId(record.getId().toHexString());
  29. commentVo.setCreateDate(DateUtil.format(new Date(record.getCreated()), "HH:mm"));
  30. //是否点赞
  31. commentVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), commentVo.getId()) ? 1 : 0);
  32. //点赞数
  33. commentVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(commentVo.getId())));
  34. for (UserInfo userInfo : userInfoList) {
  35. if(ObjectUtil.equals(record.getUserId(), userInfo.getUserId())){
  36. commentVo.setAvatar(userInfo.getLogo());
  37. commentVo.setNickname(userInfo.getNickName());
  38. break;
  39. }
  40. }
  41. result.add(commentVo);
  42. }
  43. pageResult.setItems(result);
  44. return pageResult;
  45. }
  46. /**
  47. * 发表评论
  48. * @param publishId
  49. * @param content
  50. * @return
  51. */
  52. public Boolean saveComments(String publishId, String content) {
  53. User user = UserThreadLocal.get();
  54. return this.quanZiApi.saveComment(user.getId(), publishId, content);
  55. }

5.2.4、测试

day05-圈子、小视频功能实现 - 图8
测试点赞时会发现dubbo服务中会出现null指针异常,如下:

  1. java.lang.NullPointerException
  2. at com.tanhua.dubbo.server.api.QuanZiApiImpl.saveComment(QuanZiApiImpl.java:386)
  3. at com.tanhua.dubbo.server.api.QuanZiApiImpl.likeComment(QuanZiApiImpl.java:180)
  4. at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
  5. at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
  6. at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:76)
  7. at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:52)
  8. at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)

原因是:原有的点赞实现中,需要查询Publish对象,但是现在实现的是针对评论的点赞,是查询不到Publish对象的,所以抛出了空指针异常。
解决如下:

  1. //com.tanhua.dubbo.server.api.QuanZiApiImpl
  2. /**
  3. * 保存Comment
  4. *
  5. * @return
  6. */
  7. private Boolean saveComment(Long userId, String publishId,
  8. CommentType commentType, String content) {
  9. try {
  10. Comment comment = new Comment();
  11. comment.setId(ObjectId.get());
  12. comment.setUserId(userId);
  13. comment.setPublishId(new ObjectId(publishId));
  14. // 评论类型
  15. comment.setCommentType(commentType.getType());
  16. // 内容
  17. comment.setContent(content);
  18. comment.setCreated(System.currentTimeMillis());
  19. Publish publish = this.queryPublishById(publishId);
  20. if (ObjectUtil.isNotEmpty(publish)) {
  21. comment.setPublishUserId(publish.getUserId());
  22. } else {
  23. //查询评论
  24. Comment myComment = this.queryCommentById(publishId);
  25. if(ObjectUtil.isNotEmpty(myComment)){
  26. comment.setPublishUserId(myComment.getUserId());
  27. }else{
  28. //TODO 其他情况,比如小视频等
  29. }
  30. }
  31. this.mongoTemplate.save(comment);
  32. return true;
  33. } catch (Exception e) {
  34. log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e);
  35. }
  36. return false;
  37. }
  38. /**
  39. * 根据id查询Comment对象
  40. *
  41. * @param id
  42. * @return
  43. */
  44. private Comment queryCommentById(String id) {
  45. return this.mongoTemplate.findById(new ObjectId(id), Comment.class);
  46. }

day05-圈子、小视频功能实现 - 图9
这样,点赞功能正常了。

6、小视频

6.1、功能说明

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。
效果:
day05-圈子、小视频功能实现 - 图10
查看详情:
day05-圈子、小视频功能实现 - 图11
评论:
day05-圈子、小视频功能实现 - 图12
点赞:
day05-圈子、小视频功能实现 - 图13

6.2、技术方案

对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。

  • 对于存储而言,小视频的存储量以及容量都是非常巨大的。
    • 所以我们选择自己搭建分布式存储系统 FastDFS进行存储。
  • 对于推荐算法,我们将采用多种权重的计算方式进行计算。
  • 对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速,当然了这需要额外购买CDN服务。

    7、FastDFS

    7.1、FastDFS是什么?

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

    7.2、工作原理

    FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
    Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。
    Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
    day05-圈子、小视频功能实现 - 图14
    每个 tracker 节点地位平等,收集 Storage 集群的状态。
    Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

    7.3、文件的上传

    day05-圈子、小视频功能实现 - 图15
    客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

    7.4、文件的下载

    day05-圈子、小视频功能实现 - 图16
    客户端下载请求到Tracker服务,Tracker返回给客户端storage的信息,客户端根据这些信息进行请求storage获取到文件。

    7.5、开始使用

    在我们提供的虚拟机中已经通过docker搭建了FastDFS环境,下面我们来学习下如何通过Java程序来使用FastDFS。

    7.5.1、引入依赖

    关于使用FastDFS上传小视频的逻辑我们在server工程中完成,所以需要在server工程中引入依赖。

    1. <dependency>
    2. <groupId>com.github.tobato</groupId>
    3. <artifactId>fastdfs-client</artifactId>
    4. <version>1.26.7</version>
    5. <exclusions>
    6. <exclusion>
    7. <groupId>ch.qos.logback</groupId>
    8. <artifactId>logback-classic</artifactId>
    9. </exclusion>
    10. </exclusions>
    11. </dependency>

    7.5.2、编写配置文件

    在application.properties配置文件中加入如下内容:

    1. # ===================================================================
    2. # 分布式文件系统FDFS配置
    3. # ===================================================================
    4. fdfs.so-timeout = 1501
    5. fdfs.connect-timeout = 601
    6. #缩略图生成参数
    7. fdfs.thumb-image.width= 150
    8. fdfs.thumb-image.height= 150
    9. #TrackerList参数,支持多个
    10. fdfs.tracker-list=192.168.31.81:22122
    11. #访问路径
    12. fdfs.web-server-url=http://192.168.31.81:8888/

    7.5.3、测试代码

    1. package com.tanhua.server;
    2. import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
    3. import com.github.tobato.fastdfs.domain.fdfs.StorePath;
    4. import com.github.tobato.fastdfs.service.FastFileStorageClient;
    5. import org.apache.commons.io.FileUtils;
    6. import org.junit.Test;
    7. import org.junit.runner.RunWith;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.boot.test.context.SpringBootTest;
    10. import org.springframework.test.context.junit4.SpringRunner;
    11. import java.io.File;
    12. import java.io.IOException;
    13. @RunWith(SpringRunner.class)
    14. @SpringBootTest
    15. public class TestFastDFS {
    16. @Autowired
    17. protected FastFileStorageClient storageClient;
    18. @Autowired
    19. private FdfsWebServer fdfsWebServer;
    20. @Test
    21. public void testUpload(){
    22. String path = "F:\\1.jpg";
    23. File file = new File(path);
    24. try {
    25. StorePath storePath = this.storageClient.uploadFile(FileUtils.openInputStream(file), file.length(), "jpg", null);
    26. System.out.println(storePath); //StorePath [group=group1, path=M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg]
    27. System.out.println(fdfsWebServer.getWebServerUrl() + storePath.getFullPath());//group1/M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg
    28. } catch (IOException e) {
    29. e.printStackTrace();
    30. }
    31. }
    32. }

    通过浏览器访问图片:
    day05-圈子、小视频功能实现 - 图17

    8、发布小视频

    发布小视频的流程如下:
    day05-圈子、小视频功能实现 - 图18
    说明:

  • 用户发通过客户端APP上传视频到server服务

  • server服务上传视频到FastDFS文件系统,上传成功后返回视频的url地址
  • server通过rpc的调用dubbo服务进行保存小视频数据

    8.1、dubbo服务

    8.1.1、编写pojo

    在dubbo接口工程中编写pojo:
    1. package com.tanhua.dubbo.server.pojo;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. import org.bson.types.ObjectId;
    6. import org.springframework.data.mongodb.core.mapping.Document;
    7. import java.util.List;
    8. @Data
    9. @NoArgsConstructor
    10. @AllArgsConstructor
    11. @Document(collection = "video")
    12. public class Video implements java.io.Serializable {
    13. private static final long serialVersionUID = -3136732836884933873L;
    14. private ObjectId id; //主键id
    15. private Long vid; //自增长id
    16. private Long userId;
    17. private String text; //文字
    18. private String picUrl; //视频封面文件
    19. private String videoUrl; //视频文件
    20. private Long created; //创建时间
    21. private Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
    22. private List<Long> seeList; //部分可见的列表
    23. private List<Long> notSeeList; //不给谁看的列表
    24. private String longitude; //经度
    25. private String latitude; //纬度
    26. private String locationName; //位置名称
    27. }

    8.1.2、定义接口

    1. package com.tanhua.dubbo.server.api;
    2. import com.tanhua.dubbo.server.pojo.Video;
    3. public interface VideoApi {
    4. /**
    5. * 保存小视频
    6. *
    7. * @param video
    8. * @return 保存成功后,返回视频id
    9. */
    10. String saveVideo(Video video);
    11. }

    8.1.3、编写实现

    1. package com.tanhua.dubbo.server.api;
    2. import cn.hutool.core.util.ObjectUtil;
    3. import com.alibaba.dubbo.config.annotation.Service;
    4. import com.mongodb.Mongo;
    5. import com.tanhua.dubbo.server.enums.IdType;
    6. import com.tanhua.dubbo.server.pojo.Video;
    7. import com.tanhua.dubbo.server.service.IdService;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.bson.types.ObjectId;
    10. import org.springframework.beans.factory.annotation.Autowired;
    11. import org.springframework.data.mongodb.core.MongoTemplate;
    12. @Service(version = "1.0.0")
    13. @Slf4j
    14. public class VideoApiImpl implements VideoApi {
    15. @Autowired
    16. private IdService idService;
    17. @Autowired
    18. private MongoTemplate mongoTemplate;
    19. /**
    20. * 发布小视频
    21. *
    22. * @param video
    23. * @return
    24. */
    25. @Override
    26. public String saveVideo(Video video) {
    27. try {
    28. //校验
    29. if(!ObjectUtil.isAllNotEmpty(video.getUserId(), video.getPicUrl(), video.getVideoUrl())){
    30. return null;
    31. }
    32. //设置id
    33. video.setId(ObjectId.get());
    34. video.setVid(this.idService.createId(IdType.VIDEO));
    35. //发布时间
    36. video.setCreated(System.currentTimeMillis());
    37. //保存到Mongodb中
    38. this.mongoTemplate.save(video);
    39. return video.getId().toHexString();
    40. } catch (Exception e) {
    41. log.error("小视频发布失败~ video = " + video, e);
    42. }
    43. return null;
    44. }
    45. }

    8.2、APP接口服务

    接口地址:https://mock-java.itheima.net/project/35/interface/api/821
    day05-圈子、小视频功能实现 - 图19

    8.2.1、VideoController

    1. package com.tanhua.server.controller;
    2. import com.tanhua.server.service.VideoService;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.http.HttpStatus;
    5. import org.springframework.http.ResponseEntity;
    6. import org.springframework.web.bind.annotation.PostMapping;
    7. import org.springframework.web.bind.annotation.RequestMapping;
    8. import org.springframework.web.bind.annotation.RequestParam;
    9. import org.springframework.web.bind.annotation.RestController;
    10. import org.springframework.web.multipart.MultipartFile;
    11. @RestController
    12. @RequestMapping("smallVideos")
    13. public class VideoController {
    14. @Autowired
    15. private VideoService videoService;
    16. /**
    17. * 发布小视频
    18. *
    19. * @param picFile
    20. * @param videoFile
    21. * @return
    22. */
    23. @PostMapping
    24. public ResponseEntity<Void> saveVideo(@RequestParam("videoThumbnail") MultipartFile picFile,
    25. @RequestParam("videoFile") MultipartFile videoFile) {
    26. try {
    27. Boolean bool = this.videoService.saveVideo(picFile, videoFile);
    28. if (bool) {
    29. return ResponseEntity.ok(null);
    30. }
    31. } catch (Exception e) {
    32. e.printStackTrace();
    33. }
    34. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    35. }
    36. }

    8.2.2、VideoService

    1. package com.tanhua.server.service;
    2. import cn.hutool.core.util.StrUtil;
    3. import com.alibaba.dubbo.config.annotation.Reference;
    4. import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
    5. import com.github.tobato.fastdfs.domain.fdfs.StorePath;
    6. import com.github.tobato.fastdfs.service.FastFileStorageClient;
    7. import com.tanhua.common.pojo.User;
    8. import com.tanhua.common.service.PicUploadService;
    9. import com.tanhua.common.utils.UserThreadLocal;
    10. import com.tanhua.common.vo.PicUploadResult;
    11. import com.tanhua.dubbo.server.api.VideoApi;
    12. import com.tanhua.dubbo.server.pojo.Video;
    13. import lombok.extern.slf4j.Slf4j;
    14. import org.springframework.beans.factory.annotation.Autowired;
    15. import org.springframework.stereotype.Service;
    16. import org.springframework.web.multipart.MultipartFile;
    17. @Service
    18. @Slf4j
    19. public class VideoService {
    20. @Autowired
    21. private PicUploadService picUploadService;
    22. @Autowired
    23. protected FastFileStorageClient storageClient;
    24. @Autowired
    25. private FdfsWebServer fdfsWebServer;
    26. @Reference(version = "1.0.0")
    27. private VideoApi videoApi;
    28. /**
    29. * 发布小视频
    30. *
    31. * @param picFile
    32. * @param videoFile
    33. * @return
    34. */
    35. public Boolean saveVideo(MultipartFile picFile, MultipartFile videoFile) {
    36. User user = UserThreadLocal.get();
    37. Video video = new Video();
    38. video.setUserId(user.getId());
    39. video.setSeeType(1); //默认公开
    40. try {
    41. //上传封面图片
    42. PicUploadResult picUploadResult = this.picUploadService.upload(picFile);
    43. video.setPicUrl(picUploadResult.getName()); //图片路径
    44. //上传视频
    45. StorePath storePath = storageClient.uploadFile(videoFile.getInputStream(),
    46. videoFile.getSize(),
    47. StrUtil.subAfter(videoFile.getOriginalFilename(), '.', true),
    48. null);
    49. //设置视频url
    50. video.setVideoUrl(fdfsWebServer.getWebServerUrl() + storePath.getFullPath());
    51. String videoId = this.videoApi.saveVideo(video);
    52. return StrUtil.isNotEmpty(videoId);
    53. } catch (Exception e) {
    54. log.error("发布小视频失败!file = " + picFile.getOriginalFilename() , e);
    55. }
    56. return false;
    57. }
    58. }

    5.4.3、测试

    如果上传视频,会导致异常,是因为请求太大的缘故:
    day05-圈子、小视频功能实现 - 图20
    解决:application.properties
    1. spring.servlet.multipart.max-file-size=30MB
    2. spring.servlet.multipart.max-request-size=30MB
    测试:
    day05-圈子、小视频功能实现 - 图21
    day05-圈子、小视频功能实现 - 图22
    可以看到数据已经写入到了MongoDB中。

    9、小视频列表

    小视频的列表查询的实现需要注意的是,如果有推荐视频,优先返回推荐视频,如果不够或没有,按照时间倒序查询视频表。
    推荐数据:
    day05-圈子、小视频功能实现 - 图23

    9.1、dubbo服务

    9.1.1、定义dubbo服务

    1. package com.tanhua.dubbo.server.api;
    2. import com.tanhua.dubbo.server.pojo.Video;
    3. import com.tanhua.dubbo.server.vo.PageInfo;
    4. public interface VideoApi {
    5. /**
    6. * 保存小视频
    7. *
    8. * @param video
    9. * @return
    10. */
    11. Boolean saveVideo(Video video);
    12. /**
    13. * 分页查询小视频列表,按照时间倒序排序
    14. *
    15. * @param userId
    16. * @param page
    17. * @param pageSize
    18. * @return
    19. */
    20. PageInfo<Video> queryVideoList(Long userId, Integer page, Integer pageSize);
    21. }

    9.1.2、实现dubbo服务

    1. //com.tanhua.dubbo.server.api.VideoApiImpl
    2. /**
    3. * 查询小视频列表,优先展现推荐的视频,如果没有推荐的视频或已经查询完成,就需要查询系统视频数据
    4. *
    5. * @param userId
    6. * @param page
    7. * @param pageSize
    8. * @return
    9. */
    10. @Override
    11. public PageInfo<Video> queryVideoList(Long userId, Integer page, Integer pageSize) {
    12. PageInfo<Video> pageInfo = new PageInfo<>();
    13. pageInfo.setPageNum(page);
    14. pageInfo.setPageSize(pageSize);
    15. //从redis中获取推荐视频的数据
    16. String redisKey = "QUANZI_VIDEO_RECOMMEND_" + userId;
    17. String redisData = this.redisTemplate.opsForValue().get(redisKey);
    18. List<Long> vids = new ArrayList<>();
    19. int recommendCount = 0;
    20. if (StrUtil.isNotEmpty(redisData)) {
    21. //手动分页查询数据
    22. List<String> vidList = StrUtil.split(redisData, ',');
    23. //计算分页
    24. //[0, 10]
    25. int[] startEnd = PageUtil.transToStartEnd(page - 1, pageSize);
    26. int startIndex = startEnd[0]; //开始
    27. int endIndex = Math.min(startEnd[1], vidList.size()); //结束
    28. for (int i = startIndex; i < endIndex; i++) {
    29. vids.add(Convert.toLong(vidList.get(i)));
    30. }
    31. recommendCount = vidList.size();
    32. }
    33. if (CollUtil.isEmpty(vids)) {
    34. //没有推荐或前面推荐已经查询完毕,查询系统的视频数据
    35. //计算前面的推荐视频页数
    36. int totalPage = PageUtil.totalPage(recommendCount, pageSize);
    37. PageRequest pageRequest = PageRequest.of(page - totalPage - 1, pageSize, Sort.by(Sort.Order.desc("created")));
    38. Query query = new Query().with(pageRequest);
    39. List<Video> videoList = this.mongoTemplate.find(query, Video.class);
    40. pageInfo.setRecords(videoList);
    41. return pageInfo;
    42. }
    43. //根据vid查询对应的视频数据了
    44. Query query = Query.query(Criteria.where("vid").in(vids));
    45. List<Video> videoList = this.mongoTemplate.find(query, Video.class);
    46. pageInfo.setRecords(videoList);
    47. return pageInfo;
    48. }

    9.1.3、测试用例

    1. package com.tanhua.dubbo.server.api;
    2. import com.tanhua.dubbo.server.pojo.Video;
    3. import org.junit.Test;
    4. import org.junit.runner.RunWith;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.boot.test.context.SpringBootTest;
    7. import org.springframework.test.context.junit4.SpringRunner;
    8. @RunWith(SpringRunner.class)
    9. @SpringBootTest
    10. public class TestVideoApi {
    11. @Reference(version = "1.0.0")
    12. private VideoApi videoApi;
    13. @Test
    14. public void testQueryVideoList() {
    15. //返回的推荐结果数据
    16. System.out.println(this.videoApi.queryVideoList(1L, 1, 8));
    17. //返回少于pageSize数据,因为推荐数据不够了
    18. System.out.println(this.videoApi.queryVideoList(1L, 3, 8));
    19. //返回系统数据
    20. System.out.println(this.videoApi.queryVideoList(1L, 4, 8));
    21. }
    22. }

    9.2、APP接口服务

    服务地址:https://mock-java.itheima.net/project/35/interface/api/815

    9.2.1、定义VideoVo

    1. package com.tanhua.server.vo;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.NoArgsConstructor;
    5. @Data
    6. @NoArgsConstructor
    7. @AllArgsConstructor
    8. public class VideoVo {
    9. private String id;
    10. private Long userId;
    11. private String avatar; //头像
    12. private String nickname; //昵称
    13. private String cover; //封面
    14. private String videoUrl; //视频URL
    15. private String signature; //签名
    16. private Integer likeCount; //点赞数量
    17. private Integer hasLiked; //是否已赞(1是,0否)
    18. private Integer hasFocus; //是是否关注 (1是,0否)
    19. private Integer commentCount; //评论数量
    20. }

    9.2.2、VideoController

    1. @RestController
    2. @RequestMapping("smallVideos")
    3. public class VideoController {
    4. /**
    5. * 查询小视频列表
    6. *
    7. * @param page
    8. * @param pageSize
    9. * @return
    10. */
    11. @GetMapping
    12. public ResponseEntity<PageResult> queryVideoList(@RequestParam(value = "page", defaultValue = "1") Integer page,
    13. @RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
    14. try {
    15. if (page <= 0) {
    16. page = 1;
    17. }
    18. PageResult pageResult = this.videoService.queryVideoList(page, pageSize);
    19. if (null != pageResult) {
    20. return ResponseEntity.ok(pageResult);
    21. }
    22. } catch (Exception e) {
    23. e.printStackTrace();
    24. }
    25. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    26. }
    27. }

    9.2.3、VideoService

    1. //com.tanhua.server.service.VideoService
    2. public PageResult queryVideoList(Integer page, Integer pageSize) {
    3. User user = UserThreadLocal.get();
    4. PageResult pageResult = new PageResult();
    5. pageResult.setPage(page);
    6. pageResult.setPagesize(pageSize);
    7. PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
    8. List<Video> records = pageInfo.getRecords();
    9. if(CollUtil.isEmpty(records)){
    10. return pageResult;
    11. }
    12. //查询用户信息
    13. List<Object> userIds = CollUtil.getFieldValues(records, "userId");
    14. List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);
    15. List<VideoVo> videoVoList = new ArrayList<>();
    16. for (Video record : records) {
    17. VideoVo videoVo = new VideoVo();
    18. videoVo.setUserId(record.getUserId());
    19. videoVo.setCover(record.getPicUrl());
    20. videoVo.setVideoUrl(record.getVideoUrl());
    21. videoVo.setId(record.getId().toHexString());
    22. videoVo.setSignature("我就是我~"); //TODO 签名
    23. videoVo.setCommentCount(0); //TODO 评论数
    24. videoVo.setHasFocus(0); //TODO 是否关注
    25. videoVo.setHasLiked(0); //TODO 是否点赞(1是,0否)
    26. videoVo.setLikeCount(0);//TODO 点赞数
    27. //填充用户信息
    28. for (UserInfo userInfo : userInfoList) {
    29. if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
    30. videoVo.setNickname(userInfo.getNickName());
    31. videoVo.setAvatar(userInfo.getLogo());
    32. break;
    33. }
    34. }
    35. videoVoList.add(videoVo);
    36. }
    37. pageResult.setItems(videoVoList);
    38. return pageResult;
    39. }

    9.2.5、测试

    day05-圈子、小视频功能实现 - 图24
    day05-圈子、小视频功能实现 - 图25
    可以看到已经查询到数据。下面使用手机进行测试:
    day05-圈子、小视频功能实现 - 图26