课程说明

  • 圈子功能说明
  • 圈子技术实现
  • 圈子技术方案
  • 圈子实现发布动态
  • 圈子实现好友动态
  • 圈子实现推荐动态
  • 圈子实现点赞、喜欢功能(放到后面实现)
  • 圈子实现评论(放到后面实现)
  • 圈子实现评论的点赞(放到后面实现)

1、圈子功能

1.1、功能说明

探花交友项目中的圈子功能,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。
1567496518185.png

发布:
1567497614205.png

1.2、实现方案分析

对于圈子功能的实现,我们需要对它的功能特点做分析:

  • 数据量会随着用户数增大而增大
  • 读多写少
  • 非好友看不到其动态内容
  • ……


针对以上特点,我们来分析一下:

  • 对于数据量大而言,显然不能够使用关系型数据库进行存储,我们需要通过MongoDB进行存储
  • 对于读多写少的应用,需要减少读取的成本
    • 比如说,一条SQL语句,单张表查询一定比多张表查询要快
  • 对于每个人数据在存储层面最好做到相互隔离,这样的话就不会有影响

所以对于存储而言,主要是核心的4张表:

  • 发布表:记录了所有用户的发布的东西信息,如图片、视频等。
  • 相册:相册是每个用户独立的,记录了该用户所发布的所有内容。
  • 评论:针对某个具体发布的朋友评论和点赞操作。
  • 时间线:所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。

1.3、技术方案

根据之前我们的分析,对于技术方案而言,将采用MongoDB+Redis来实现,其中MongoDB负责存储,Redis负责缓存数据。

1.3.1、发布流程

1567523191578.png

流程说明:

  • 用户发布动态,首先将动态内容写入到发布表。
  • 然后,将发布的指向写入到自己的相册表中。
  • 最后,将发布的指向写入到好友的时间线中。

1.3.2、查看流程

1567525088273.png

流程说明:

  • 用户查看动态,如果查看自己的动态,直接查询相册表即可
  • 如果查看好友动态,查询时间线表即可
  • 如果查看推荐动态,查看推荐表即可


由此可见,查看动态的成本较低,可以快速的查询到动态数据。

1.4、表结构设计

发布表:

  1. #表名:quanzi_publish
  2. {
  3. "id":1,#主键id
  4. "userId":1, #用户id
  5. "text":"今天心情很好", #文本内容
  6. "medias":"http://xxxx/x/y/z.jpg", #媒体数据,图片或小视频 url
  7. "seeType":1, #谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
  8. "seeList":[1,2,3], #部分可见的列表
  9. "notSeeList":[4,5,6],#不给谁看的列表
  10. "longitude":108.840974298098,#经度
  11. "latitude":34.2789316522934,#纬度
  12. "locationName":"上海市浦东区", #位置名称
  13. "created",1568012791171 #发布时间
  14. }

相册表:

  1. #表名:quanzi_album_{userId}
  2. {
  3. "id":1,#主键id
  4. "publishId":1001, #发布表主键id
  5. "created":1568012791171 #发布时间
  6. }

时间线表:

  1. #表名:quanzi_time_line_{userId}
  2. {
  3. "id":1,#主键id,
  4. "userId":2, #好友id
  5. "publishId":1001, #发布表主键id
  6. "date":1568012791171 #发布时间
  7. }

评论表:

  1. #表名:quanzi_comment
  2. {
  3. "id":1, #主键id
  4. "publishId":1001, #发布表主键id
  5. "commentType":1, #评论类型,1-点赞,2-评论,3-喜欢
  6. "content":"给力!", #评论内容
  7. "userId":2, #评论人
  8. "isParent":false, #是否为父节点,默认是否
  9. "parentId":1001, #父节点id
  10. "created":1568012791171
  11. }

1.5、关于海量数据的探讨

通过以上表结构的设计,可以满足我们现在的需求,但是,我们需要思考一个问题,如果我们真的拥有了海量数据,会给我们带来什么挑战?

在上述的表设计中,其实是2种设计思路,一种是合表存储,另外一种是分表存储。无论是分表还是合表存储,在面临海量数据时都会有很大的压力,那么我们该怎么面对呢?

MongoDB出现就是在解决海量数据存储问题的,那么MongoDB是如何解决的呢? 答案是:集群

MongoDB有三种集群方式,分别是:主从集群、副本集群、分片式集群,其中主从集群官方已经不推荐了,一般用副本集群取代主从集群。

1.5.1、副本集群

一个主,两个从库组成,主库宕机时,这两个从库都可以被选为主库。
1190037-20180106145148128-1854811460.png

当主库宕机后,两个从库都会进行竞选,其中一个变为主库,当原主库恢复后,作为从库加入当前的复制集群即可。
1190037-20180106145154284-397901575.png

1.5.2、分片集群

分片(sharding)是MongoDB用来将大型集合分割到不同服务器(或者说一个集群)上所采用的方法。

例如,如果数据库1tb的数据集,并有4个分片,然后每个分片可能仅持有256 GB的数据。如果有40个分片,那么每个切分可能只有25GB的数据。
1190037-20180106150209471-1233466151.png

MongoDB中数据的分片是以集合为基本单位的,集合中的数据通过片键(Shard key)被分成多部分。其实片键就是在集合中选一个键,用该键的值作为数据拆分的依据。

一般片键采用范围或哈希的方式进行分片。

1.5.3、解决问题

了解完MongoDB的集群方案后,为了实现海量数据存储的需求,我们应该选择分片式集群,下面我们探讨下圈子的表设计。

  • 发布表(quanzi_publish)
    • 建议选择userId作为片键。
  • 评论表(quanzi_comment)
    • 建议选择publishId作为片键。
  • 相册表(quanzialbum{userId})
    • 由于MongoDB的分片是集群集合的,所以需要将相册表的数据写入到一个集合中,按照userId进行分片。(增加userId字段)
  • 时间线表(quanzitime_line{userId})
    • 与相册相同,需要将数据写入到一个集合,按照my_userId进行分片。(增加my_userId字段)

2、圈子实现

升级Genymotion: Genymotion版本:3.0.2 镜像版本:1567688597143.png

2.1、pojo

写到dubbo工程中:

  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.Date;
  8. import java.util.List;
  9. /**
  10. * 此代码写到dubbo工程下的interface下
  11. * 发布表,动态内容
  12. */
  13. @Data
  14. @NoArgsConstructor
  15. @AllArgsConstructor
  16. @Document(collection = "quanzi_publish")
  17. public class Publish implements java.io.Serializable {
  18. private static final long serialVersionUID = 8732308321082804771L;
  19. private ObjectId id; //主键id
  20. private Long userId;
  21. private String text; //文字
  22. private List<String> medias; //媒体数据,图片或小视频 url
  23. private Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
  24. private List<Long> seeList; //部分可见的列表
  25. private List<Long> notSeeList; //不给谁看的列表
  26. private String longitude; //经度
  27. private String latitude; //纬度
  28. private String locationName; //位置名称
  29. private Long created; //发布时间
  30. }
  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.Date;
  8. /**
  9. * 相册表,用于存储自己发布的数据,每一个用户一张表进行存储
  10. */
  11. @Data
  12. @NoArgsConstructor
  13. @AllArgsConstructor
  14. @Document(collection = "quanzi_album")
  15. public class Album implements java.io.Serializable {
  16. private static final long serialVersionUID = 432183095092216817L;
  17. private ObjectId id; //主键id
  18. private ObjectId publishId; //发布id
  19. private Long created; //发布时间
  20. }
  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.Date;
  8. /**
  9. * 时间线表,用于存储发布(或推荐)的数据,每一个用户一张表进行存储
  10. */
  11. @Data
  12. @NoArgsConstructor
  13. @AllArgsConstructor
  14. @Document(collection = "quanzi_time_line")
  15. public class TimeLine implements java.io.Serializable{
  16. private static final long serialVersionUID = 9096178416317502524L;
  17. private ObjectId id;
  18. private Long userId; // 好友id
  19. private ObjectId publishId; //发布id
  20. private Long date; //发布的时间
  21. }
  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.Date;
  8. /**
  9. * 评论表
  10. */
  11. @Data
  12. @NoArgsConstructor
  13. @AllArgsConstructor
  14. @Document(collection = "quanzi_comment")
  15. public class Comment implements java.io.Serializable{
  16. private static final long serialVersionUID = -291788258125767614L;
  17. private ObjectId id;
  18. private ObjectId publishId; //发布id
  19. private Integer commentType; //评论类型,1-点赞,2-评论,3-喜欢
  20. private String content; //评论内容
  21. private Long userId; //发布评论的评论人的id
  22. private Boolean isParent = false; //是否为父节点,默认是否
  23. private ObjectId parentId; // 父节点id
  24. private Long created; //发表时间
  25. }
  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.Date;
  8. /**
  9. * 用户的好友列表映射的javabean类
  10. */
  11. @Data
  12. @NoArgsConstructor
  13. @AllArgsConstructor
  14. @Document(collection = "tanhua_users")
  15. public class Users implements java.io.Serializable{
  16. private static final long serialVersionUID = 6003135946820874230L;
  17. private ObjectId id;
  18. private Long userId; //用户id
  19. private Long friendId; //好友id
  20. private Long date; //时间
  21. }

2.2、发布动态

2.2.1、定义接口

  1. package com.tanhua.dubbo.server.api;
  2. import com.tanhua.dubbo.server.pojo.Publish;
  3. public interface QuanZiApi {
  4. /**
  5. * 发布动态的接口
  6. *
  7. * @param publish 要发布的信息
  8. * @return 是否发布成功
  9. */
  10. boolean savePublish(Publish publish);
  11. }

2.2.2、编写实现

在测试环境下构造好友数据(包名建议和接口的保持一致):

  1. package com.tanhua.dubbo.server.api;
  2. import com.tanhua.dubbo.server.pojo.Users;
  3. import org.bson.types.ObjectId;
  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.data.mongodb.core.MongoTemplate;
  9. import org.springframework.data.mongodb.core.query.Criteria;
  10. import org.springframework.data.mongodb.core.query.Query;
  11. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  12. import java.util.Date;
  13. import java.util.List;
  14. @RunWith(SpringJUnit4ClassRunner.class)
  15. @SpringBootTest
  16. public class TestUsers {
  17. @Autowired
  18. private MongoTemplate mongoTemplate;
  19. @Test
  20. public void saveUsers(){
  21. this.mongoTemplate.save(new Users(ObjectId.get(),1L, 2L, System.currentTimeMillis()));
  22. this.mongoTemplate.save(new Users(ObjectId.get(),1L, 3L, System.currentTimeMillis()));
  23. this.mongoTemplate.save(new Users(ObjectId.get(),1L, 4L, System.currentTimeMillis()));
  24. this.mongoTemplate.save(new Users(ObjectId.get(),1L, 5L, System.currentTimeMillis()));
  25. this.mongoTemplate.save(new Users(ObjectId.get(),1L, 6L, System.currentTimeMillis()));
  26. }
  27. @Test
  28. public void testQueryList(){
  29. Criteria criteria = Criteria.where("userId").is(1L);
  30. List<Users> users = this.mongoTemplate.find(Query.query(criteria), Users.class);
  31. for (Users user : users) {
  32. System.out.println(user);
  33. }
  34. }
  35. }

实现发布:

  1. package com.tanhua.dubbo.server.api;
  2. import com.alibaba.dubbo.config.annotation.Service;
  3. import com.tanhua.dubbo.server.pojo.Album;
  4. import com.tanhua.dubbo.server.pojo.Publish;
  5. import com.tanhua.dubbo.server.pojo.TimeLine;
  6. import com.tanhua.dubbo.server.pojo.Users;
  7. import org.bson.types.ObjectId;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.data.mongodb.core.MongoTemplate;
  10. import org.springframework.data.mongodb.core.query.Criteria;
  11. import org.springframework.data.mongodb.core.query.Query;
  12. import java.util.Date;
  13. import java.util.List;
  14. @Service(version = "1.0.0")
  15. public class QuanZiApiImpl implements QuanZiApi {
  16. @Autowired
  17. private MongoTemplate mongoTemplate;
  18. @Override
  19. public boolean savePublish(Publish publish) {
  20. // 校验
  21. if (publish.getUserId() == null) {
  22. return false;
  23. }
  24. try {
  25. publish.setCreated(System.currentTimeMillis()); //设置创建时间
  26. publish.setId(ObjectId.get()); //设置id
  27. this.mongoTemplate.save(publish); //保存发布
  28. Album album = new Album(); // 构建相册对象
  29. album.setPublishId(publish.getId());
  30. album.setCreated(System.currentTimeMillis());
  31. album.setId(ObjectId.get());
  32. this.mongoTemplate.save(album, "quanzi_album_" + publish.getUserId());
  33. //写入好友的时间线中
  34. Criteria criteria = Criteria.where("userId").is(publish.getUserId());
  35. List<Users> users = this.mongoTemplate.find(Query.query(criteria), Users.class);
  36. for (Users user : users) {
  37. TimeLine timeLine = new TimeLine();
  38. timeLine.setId(ObjectId.get());
  39. timeLine.setPublishId(publish.getId());
  40. //设置好友的id(相当于把自己的id写入到朋友时间线的好友id中)
  41. timeLine.setUserId(user.getUserId());
  42. timeLine.setDate(System.currentTimeMillis());
  43. this.mongoTemplate.save(timeLine, "quanzi_time_line_" + user.getFriendId());
  44. }
  45. return true;
  46. } catch (Exception e) {
  47. e.printStackTrace();
  48. //TODO 出错的事务回滚,MongoDB非集群不支持事务,暂不进行实现
  49. }
  50. return false;
  51. }
  52. }

测试用例:

  1. @Test
  2. public void testSavePublish(){
  3. Publish publish = new Publish();
  4. publish.setUserId(1L);
  5. publish.setLocationName("上海市");
  6. publish.setSeeType(1);
  7. publish.setText("今天天气不错~");
  8. publish.setMedias(Arrays.asList("https://itcast-tanhua.oss-cn-shanghai.aliyuncs.com/images/quanzi/1.jpg"));
  9. boolean result = this.quanZiApi.savePublish(publish);
  10. System.out.println(result);
  11. }

2.2.3、编写接口服务

在服务工程中编写接口服务。

2.2.3.1、Controller
  1. package com.tanhua.server.controller;
  2. import com.tanhua.server.service.MovementsService;
  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.*;
  7. import org.springframework.web.multipart.MultipartFile;
  8. @RestController
  9. @RequestMapping("movements")
  10. public class MovementsController {
  11. @Autowired
  12. private MovementsService movementsService;
  13. /**
  14. * 发送动态
  15. *
  16. * @param textContent
  17. * @param location
  18. * @param multipartFile
  19. * @param token
  20. * @return
  21. */
  22. @PostMapping()
  23. public ResponseEntity<Void> savePublish(@RequestParam(value = "textContent", required = false) String textContent,
  24. @RequestParam(value = "location", required = false) String location,
  25. @RequestParam(value = "latitude", required = false) String latitude,
  26. @RequestParam(value = "longitude", required = false) String longitude,
  27. @RequestParam(value = "imageContent", required = false) MultipartFile[] multipartFile,
  28. @RequestHeader("Authorization") String token) {
  29. try {
  30. boolean result = this.movementsService.savePublish(textContent, location,latitude, longitude, multipartFile, token);
  31. if(result){
  32. return ResponseEntity.ok(null);
  33. }
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. }
  37. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  38. }
  39. }

2.2.3.2、MovementsService
  1. package com.tanhua.server.service;
  2. import com.alibaba.dubbo.config.annotation.Reference;
  3. import com.tanhua.common.pojo.User;
  4. import com.tanhua.server.service.PicUploadService;
  5. import com.tanhua.common.vo.PicUploadResult;
  6. import com.tanhua.dubbo.server.api.QuanZiApi;
  7. import com.tanhua.dubbo.server.pojo.Publish;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Service;
  10. import org.springframework.web.multipart.MultipartFile;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. @Service
  14. public class MovementsService {
  15. @Reference(version = "1.0.0")
  16. private QuanZiApi quanZiApi;
  17. @Autowired
  18. private PicUploadService picUploadService;
  19. @Autowired
  20. private UserService userService;
  21. public boolean savePublish(String textContent,
  22. String location,
  23. String latitude,
  24. String longitude,
  25. MultipartFile[] multipartFile,
  26. String token) {
  27. //查询当前的登录信息
  28. User user = this.userService.queryUserByToken(token);
  29. if (null == user) {
  30. return false;
  31. }
  32. Publish publish = new Publish();
  33. publish.setUserId(user.getId());
  34. publish.setText(textContent);
  35. publish.setLocationName(location);
  36. publish.setLatitude(latitude);
  37. publish.setLongitude(longitude);
  38. publish.setSeeType(1);
  39. List<String> picUrls = new ArrayList<>();
  40. //图片上传
  41. for (MultipartFile file : multipartFile) {
  42. PicUploadResult picUploadResult = this.picUploadService.upload(file);
  43. picUrls.add(picUploadResult.getName());
  44. }
  45. publish.setMedias(picUrls);
  46. return this.quanZiApi.savePublish(publish);
  47. }
  48. }

2.2.3.3、PicUploadService

导入所需依赖:

  1. <dependency>
  2. <groupId>com.aliyun.oss</groupId>
  3. <artifactId>aliyun-sdk-oss</artifactId>
  4. <version>2.8.3</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>joda-time</groupId>
  8. <artifactId>joda-time</artifactId>
  9. </dependency>
  1. package com.tanhua.server.service;
  2. import com.aliyun.oss.OSSClient;
  3. import com.tanhua.server.config.AliyunConfig;
  4. import com.tanhua.server.vo.PicUploadResult;
  5. import org.apache.commons.lang3.RandomUtils;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.joda.time.DateTime;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Service;
  10. import org.springframework.web.multipart.MultipartFile;
  11. import java.io.ByteArrayInputStream;
  12. @Service
  13. public class PicUploadService {
  14. // 允许上传的格式
  15. private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",
  16. ".jpeg", ".gif", ".png"};
  17. @Autowired
  18. private OSSClient ossClient;
  19. @Autowired
  20. private AliyunConfig aliyunConfig;
  21. public PicUploadResult upload(MultipartFile uploadFile) {
  22. PicUploadResult fileUploadResult = new PicUploadResult();
  23. //图片做校验,对后缀名
  24. boolean isLegal = false;
  25. for (String type : IMAGE_TYPE) {
  26. if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(),
  27. type)) {
  28. isLegal = true;
  29. break;
  30. }
  31. }
  32. if (!isLegal) {
  33. fileUploadResult.setStatus("error");
  34. return fileUploadResult;
  35. }
  36. // 文件新路径
  37. String fileName = uploadFile.getOriginalFilename();
  38. String filePath = getFilePath(fileName);
  39. // 上传到阿里云
  40. try {
  41. // 目录结构:images/2018/12/29/xxxx.jpg
  42. ossClient.putObject(aliyunConfig.getBucketName(), filePath, new
  43. ByteArrayInputStream(uploadFile.getBytes()));
  44. } catch (Exception e) {
  45. e.printStackTrace();
  46. //上传失败
  47. fileUploadResult.setStatus("error");
  48. return fileUploadResult;
  49. }
  50. // 上传成功
  51. fileUploadResult.setStatus("done");
  52. fileUploadResult.setName(this.aliyunConfig.getUrlPrefix() + filePath);
  53. fileUploadResult.setUid(String.valueOf(System.currentTimeMillis()));
  54. return fileUploadResult;
  55. }
  56. private String getFilePath(String sourceFileName) {
  57. DateTime dateTime = new DateTime();
  58. return "images/" + dateTime.toString("yyyy")
  59. + "/" + dateTime.toString("MM") + "/"
  60. + dateTime.toString("dd") + "/" + System.currentTimeMillis() +
  61. RandomUtils.nextInt(100, 9999) + "." +
  62. StringUtils.substringAfterLast(sourceFileName, ".");
  63. }
  64. }

2.2.3.4、AliyunConfig
  1. package com.tanhua.server.config;
  2. import com.aliyun.oss.OSSClient;
  3. import lombok.Data;
  4. import org.springframework.boot.context.properties.ConfigurationProperties;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.context.annotation.PropertySource;
  8. @Configuration
  9. @PropertySource("classpath:aliyun.properties")
  10. @ConfigurationProperties(prefix = "aliyun")
  11. @Data
  12. public class AliyunConfig {
  13. private String endpoint;
  14. private String accessKeyId;
  15. private String accessKeySecret;
  16. private String bucketName;
  17. private String urlPrefix;
  18. @Bean
  19. public OSSClient oSSClient() {
  20. return new OSSClient(endpoint, accessKeyId, accessKeySecret);
  21. }
  22. }

2.2.3.5、aliyun.properties
  1. aliyun.endpoint = http://oss-cn-shanghai.aliyuncs.com
  2. aliyun.accessKeyId = xxxxx
  3. aliyun.accessKeySecret = xxxx
  4. aliyun.bucketName=itcast-tanhua
  5. aliyun.urlPrefix=http://itcast-tanhua.oss-cn-shanghai.aliyuncs.com/

2.2.3.5、PicUploadResult
  1. package com.tanhua.server.vo;
  2. import lombok.Data;
  3. @Data
  4. public class PicUploadResult {
  5. // 文件唯一标识
  6. private String uid;
  7. // 文件名
  8. private String name;
  9. // 状态有:uploading done error removed
  10. private String status;
  11. // 服务端响应内容,如:'{"status": "success"}'
  12. private String response;
  13. }

2.2.4、测试

day04-圈子功能实现 - 图10

day04-圈子功能实现 - 图11

结果:day04-圈子功能实现 - 图12

2.2.5、整合测试

day04-圈子功能实现 - 图13

day04-圈子功能实现 - 图14

2.3、统一处理token

在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,接下来我们将使用拦截器+ThreadLocal的方式进行解决。

2.3.1、编写UserThreadLocal

  1. package com.tanhua.server.utils;
  2. import com.tanhua.server.pojo.User;
  3. public class UserThreadLocal {
  4. private static final ThreadLocal<User> LOCAL = new ThreadLocal<User>();
  5. private UserThreadLocal() {
  6. }
  7. public static void set(User user) {
  8. LOCAL.set(user);
  9. }
  10. public static User get() {
  11. return LOCAL.get();
  12. }
  13. }

2.3.2、编写TokenInterceptor

  1. package com.tanhua.server.interceptor;
  2. import com.tanhua.server.pojo.User;
  3. import com.tanhua.server.service.UserService;
  4. import com.tanhua.server.utils.NoAuthorization;
  5. import com.tanhua.server.utils.UserThreadLocal;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Component;
  9. import org.springframework.web.method.HandlerMethod;
  10. import org.springframework.web.servlet.HandlerInterceptor;
  11. import javax.servlet.http.HttpServletRequest;
  12. import javax.servlet.http.HttpServletResponse;
  13. /**
  14. * 统一完成根据token查询用User的功能
  15. */
  16. @Component
  17. public class TokenInterceptor implements HandlerInterceptor {
  18. @Autowired
  19. private UserService userService;
  20. @Override
  21. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
  22. throws Exception {
  23. if (handler instanceof HandlerMethod) {
  24. HandlerMethod handlerMethod = (HandlerMethod) handler;
  25. NoAuthorization noAnnotation = handlerMethod.getMethod().getAnnotation(NoAuthorization.class);
  26. if (noAnnotation != null) {
  27. // 如果该方法被标记为无需验证token,直接返回即可
  28. return true;
  29. }
  30. }
  31. String token = request.getHeader("Authorization");
  32. if (StringUtils.isNotEmpty(token)) {
  33. User user = this.userService.queryUserByToken(token);
  34. if (null != user) {
  35. UserThreadLocal.set(user); //将当前对象,存储到当前的线程中
  36. return true;
  37. }
  38. }
  39. //请求头中如不存在Authorization直接返回false
  40. response.setStatus(401); //无权限访问
  41. return false;
  42. }
  43. }

2.3.3、编写注解NoAuthorization

  1. package com.tanhua.server.utils;
  2. import java.lang.annotation.*;
  3. @Target(ElementType.METHOD)
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented //标记注解
  6. public @interface NoAuthorization {
  7. }

2.3.4、注册拦截器

  1. package com.tanhua.server.config;
  2. import com.tanhua.server.interceptor.RedisCacheInterceptor;
  3. import com.tanhua.server.interceptor.TokenInterceptor;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  7. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  8. @Configuration
  9. public class WebConfig implements WebMvcConfigurer {
  10. @Autowired
  11. private RedisCacheInterceptor redisCacheInterceptor;
  12. @Autowired
  13. private TokenInterceptor tokenInterceptor;
  14. @Override
  15. public void addInterceptors(InterceptorRegistry registry) {
  16. // 注意拦截器的顺序
  17. registry.addInterceptor(this.tokenInterceptor).addPathPatterns("/**");
  18. registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
  19. }
  20. }

2.3.5、使用ThreadLocal

day04-圈子功能实现 - 图15

2.4、查询好友动态

查询好友动态其实就是查询自己的时间线表,好友在发动态时已经将动态信息写入到了自己的时间线表中。

2.4.1、编写dubbo接口

  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. *
  8. * @param publish
  9. * @return
  10. */
  11. boolean savePublish(Publish publish);
  12. /**
  13. * 查询动态
  14. *
  15. * @return
  16. */
  17. PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize);
  18. }

2.4.2、编写实现

  1. @Override
  2. public PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize) {
  3. PageRequest pageRequest = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.desc("created")));
  4. Query query = new Query().with(pageRequest);
  5. //查询时间线表
  6. List<TimeLine> timeLineList = this.mongoTemplate.find(query, TimeLine.class, "quanzi_time_line_" + userId);
  7. List<ObjectId> publishIds = new ArrayList<>();
  8. for (TimeLine timeLine : timeLineList) {
  9. publishIds.add(timeLine.getPublishId());
  10. }
  11. //查询发布信息
  12. Query queryPublish = Query.query(Criteria.where("id").in(publishIds)).with(Sort.by(Sort.Order.desc("created")));
  13. List<Publish> publishList = this.mongoTemplate.find(queryPublish, Publish.class);
  14. PageInfo<Publish> pageInfo = new PageInfo<>();
  15. pageInfo.setPageNum(page);
  16. pageInfo.setPageSize(pageSize);
  17. pageInfo.setRecords(publishList);
  18. pageInfo.setTotal(0); //不提供总数
  19. return pageInfo;
  20. }

2.4.3、编写接口服务

在itcast-tanhua-server中完成。

  1. /**
  2. * 查询好友动态
  3. *
  4. * @param page
  5. * @param pageSize
  6. * @return
  7. */
  8. @GetMapping
  9. public PageResult queryPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,
  10. @RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
  11. return this.movementsService.queryPublishList(page, pageSize);
  12. }

2.4.4、编写movementsService

TODO的内容,在后面实现。

  1. /**
  2. * 查询好友动态
  3. *
  4. * @param page
  5. * @param pageSize
  6. * @return
  7. */
  8. public PageResult queryPublishList(Integer page, Integer pageSize) {
  9. PageResult pageResult = new PageResult();
  10. //获取当前的登录信息
  11. User user = UserThreadLocal.get();
  12. PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(user.getId(), page, pageSize);
  13. pageResult.setPagesize(pageSize);
  14. pageResult.setPage(page);
  15. pageResult.setCounts(0);
  16. pageResult.setPages(0);
  17. List<Publish> records = pageInfo.getRecords();
  18. if (CollectionUtils.isEmpty(records)) {
  19. //没有动态信息
  20. return pageResult;
  21. }
  22. List<Movements> movementsList = new ArrayList<>();
  23. for (Publish record : records) {
  24. Movements movements = new Movements();
  25. //设置动态id
  26. movements.setId(record.getId().toHexString());
  27. //设置图片信息
  28. movements.setImageContent(record.getMedias().toArray(new String[]{}));
  29. //设置文字信息
  30. movements.setTextContent(record.getText());
  31. //设置用户id
  32. movements.setUserId(record.getUserId());
  33. //设置创建的时间信息
  34. movements.setCreateDate(RelativeDateFormat.format(new Date(record.getCreated())));
  35. movementsList.add(movements);
  36. }
  37. //创建集合存储用户id
  38. List<Long> userIds = new ArrayList<>();
  39. for (Movements movements : movementsList) {
  40. if(!userIds.contains(movements.getId())){
  41. userIds.add(movements.getUserId());
  42. }
  43. }
  44. QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
  45. queryWrapper.in("user_id", userIds);
  46. //查询好友信息
  47. List<UserInfo> userInfos = this.userInfoService.queryList(queryWrapper);
  48. for (Movements movements : movementsList) {
  49. for (UserInfo userInfo : userInfos) {
  50. if (movements.getUserId().longValue() == userInfo.getUserId().longValue()) {
  51. movements.setAge(userInfo.getAge());
  52. movements.setAvatar(userInfo.getLogo());
  53. movements.setGender(userInfo.getSex().name().toLowerCase());
  54. movements.setNickname(userInfo.getNickName());
  55. movements.setTags(StringUtils.split(userInfo.getTags(), ','));
  56. movements.setCommentCount(10); //TODO 评论数
  57. movements.setDistance("1.2公里"); //TODO 距离
  58. movements.setHasLiked(1); //TODO 是否点赞(1是,0否)
  59. movements.setHasLoved(0); //TODO 是否喜欢(1是,0否)
  60. movements.setLikeCount(100); //TODO 点赞数
  61. movements.setLoveCount(80); //TODO 喜欢数
  62. break;
  63. }
  64. }
  65. }
  66. pageResult.setItems(movementsList);
  67. return pageResult;
  68. }
  1. package com.tanhua.server.utils;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Date;
  5. public class RelativeDateFormat {
  6. //一分钟
  7. private static final long ONE_MINUTE = 60000L;
  8. //一小时
  9. private static final long ONE_HOUR = 3600000L;
  10. //一天
  11. private static final long ONE_DAY = 86400000L;
  12. //一周
  13. private static final long ONE_WEEK = 604800000L;
  14. private static final String ONE_SECOND_AGO = "秒前";
  15. private static final String ONE_MINUTE_AGO = "分钟前";
  16. private static final String ONE_HOUR_AGO = "小时前";
  17. private static final String ONE_DAY_AGO = "天前";
  18. private static final String ONE_MONTH_AGO = "月前";
  19. private static final String ONE_YEAR_AGO = "年前";
  20. public static void main(String[] args) throws ParseException {
  21. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:m:s");
  22. Date date = format.parse("2013-11-11 18:35:35");
  23. System.out.println(format(date));
  24. }
  25. public static String format(Date date) {
  26. //用当前时间减去之前创建的时间
  27. long delta = new Date().getTime() - date.getTime();
  28. if (delta < 1L * ONE_MINUTE) {
  29. long seconds = toSeconds(delta);
  30. return (seconds <= 0 ? 1 : seconds) + ONE_SECOND_AGO;
  31. }
  32. if (delta < 45L * ONE_MINUTE) {
  33. long minutes = toMinutes(delta);
  34. return (minutes <= 0 ? 1 : minutes) + ONE_MINUTE_AGO;
  35. }
  36. if (delta < 24L * ONE_HOUR) {
  37. long hours = toHours(delta);
  38. return (hours <= 0 ? 1 : hours) + ONE_HOUR_AGO;
  39. }
  40. if (delta < 48L * ONE_HOUR) {
  41. return "昨天";
  42. }
  43. if (delta < 30L * ONE_DAY) {
  44. long days = toDays(delta);
  45. return (days <= 0 ? 1 : days) + ONE_DAY_AGO;
  46. }
  47. if (delta < 12L * 4L * ONE_WEEK) {
  48. long months = toMonths(delta);
  49. return (months <= 0 ? 1 : months) + ONE_MONTH_AGO;
  50. } else {
  51. long years = toYears(delta);
  52. return (years <= 0 ? 1 : years) + ONE_YEAR_AGO;
  53. }
  54. }
  55. private static long toSeconds(long date) {
  56. return date / 1000L;
  57. }
  58. private static long toMinutes(long date) {
  59. return toSeconds(date) / 60L;
  60. }
  61. private static long toHours(long date) {
  62. return toMinutes(date) / 60L;
  63. }
  64. private static long toDays(long date) {
  65. return toHours(date) / 24L;
  66. }
  67. private static long toMonths(long date) {
  68. return toDays(date) / 30L;
  69. }
  70. private static long toYears(long date) {
  71. return toMonths(date) / 365L;
  72. }
  73. }

Movements对象:

  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 Movements {
  9. private String id; //动态id
  10. private Long userId; //用户id
  11. private String avatar; //头像
  12. private String nickname; //昵称
  13. private String gender; //性别 man woman
  14. private Integer age; //年龄
  15. private String[] tags; //标签
  16. private String textContent; //文字动态
  17. private String[] imageContent; //图片动态
  18. private String distance; //距离
  19. private String createDate; //发布时间 如: 10分钟前
  20. private Integer likeCount; //点赞数
  21. private Integer commentCount; //评论数
  22. private Integer loveCount; //喜欢数
  23. private Integer hasLiked; //是否点赞(1是,0否)
  24. private Integer hasLoved; //是否喜欢(1是,0否)
  25. }

2.4.5、测试

day04-圈子功能实现 - 图16

2.5、查询推荐动态

推荐动态是通过推荐系统计算出的结果,现在我们只需要实现查询即可,推荐系统在后面的课程中完成。

推荐动态和好友动态的结构是一样的,所以我们只需要查询推荐的时间表即可。

2.5.1、修改dubbo服务逻辑 day04-圈子功能实现 - 图17

2.5.2、编写测试用例

该测试用例用于插入推荐数据。

  1. @Test
  2. public void testRecommendPublish(){
  3. //查询用户id为2的动态作为推荐动态的数据
  4. PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(2L, 1, 10);
  5. for (Publish record : pageInfo.getRecords()) {
  6. TimeLine timeLine = new TimeLine();
  7. timeLine.setId(ObjectId.get());
  8. timeLine.setPublishId(record.getId());
  9. timeLine.setUserId(record.getUserId());
  10. timeLine.setDate(System.currentTimeMillis());
  11. this.mongoTemplate.save(timeLine, "quanzi_time_line_recommend");
  12. }
  13. }

2.5.3、MovementsController

  1. /**
  2. * 查询推荐动态
  3. *
  4. * @param page
  5. * @param pageSize
  6. * @return
  7. */
  8. @GetMapping("recommend")
  9. public PageResult queryRecommendPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,
  10. @RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
  11. return this.movementsService.queryPublishList(page, pageSize, true);
  12. }

2.5.4、MovementsService

day04-圈子功能实现 - 图18

2.5.5、整合测试

day04-圈子功能实现 - 图19