课程说明
- 圈子功能说明
- 圈子技术实现
- 圈子技术方案
- 圈子实现发布动态
- 圈子实现好友动态
- 圈子实现推荐动态
- 圈子实现点赞、喜欢功能(放到后面实现)
- 圈子实现评论(放到后面实现)
- 圈子实现评论的点赞(放到后面实现)
1、圈子功能
1.1、功能说明
探花交友项目中的圈子功能,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。
发布:
1.2、实现方案分析
对于圈子功能的实现,我们需要对它的功能特点做分析:
- 数据量会随着用户数增大而增大
- 读多写少
- 非好友看不到其动态内容
- ……
针对以上特点,我们来分析一下:
- 对于数据量大而言,显然不能够使用关系型数据库进行存储,我们需要通过MongoDB进行存储
- 对于读多写少的应用,需要减少读取的成本
- 比如说,一条SQL语句,单张表查询一定比多张表查询要快
- 对于每个人数据在存储层面最好做到相互隔离,这样的话就不会有影响
所以对于存储而言,主要是核心的4张表:
- 发布表:记录了所有用户的发布的东西信息,如图片、视频等。
- 相册:相册是每个用户独立的,记录了该用户所发布的所有内容。
- 评论:针对某个具体发布的朋友评论和点赞操作。
- 时间线:所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。
1.3、技术方案
根据之前我们的分析,对于技术方案而言,将采用MongoDB+Redis来实现,其中MongoDB负责存储,Redis负责缓存数据。
1.3.1、发布流程

流程说明:
- 用户发布动态,首先将动态内容写入到发布表。
- 然后,将发布的指向写入到自己的相册表中。
- 最后,将发布的指向写入到好友的时间线中。
1.3.2、查看流程

流程说明:
- 用户查看动态,如果查看自己的动态,直接查询相册表即可
- 如果查看好友动态,查询时间线表即可
- 如果查看推荐动态,查看推荐表即可
由此可见,查看动态的成本较低,可以快速的查询到动态数据。
1.4、表结构设计
发布表:
#表名:quanzi_publish{"id":1,#主键id"userId":1, #用户id"text":"今天心情很好", #文本内容"medias":"http://xxxx/x/y/z.jpg", #媒体数据,图片或小视频 url"seeType":1, #谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看"seeList":[1,2,3], #部分可见的列表"notSeeList":[4,5,6],#不给谁看的列表"longitude":108.840974298098,#经度"latitude":34.2789316522934,#纬度"locationName":"上海市浦东区", #位置名称"created",1568012791171 #发布时间}
相册表:
#表名:quanzi_album_{userId}{"id":1,#主键id"publishId":1001, #发布表主键id"created":1568012791171 #发布时间}
时间线表:
#表名:quanzi_time_line_{userId}{"id":1,#主键id,"userId":2, #好友id"publishId":1001, #发布表主键id"date":1568012791171 #发布时间}
评论表:
#表名:quanzi_comment{"id":1, #主键id"publishId":1001, #发布表主键id"commentType":1, #评论类型,1-点赞,2-评论,3-喜欢"content":"给力!", #评论内容"userId":2, #评论人"isParent":false, #是否为父节点,默认是否"parentId":1001, #父节点id"created":1568012791171}
1.5、关于海量数据的探讨
通过以上表结构的设计,可以满足我们现在的需求,但是,我们需要思考一个问题,如果我们真的拥有了海量数据,会给我们带来什么挑战?
在上述的表设计中,其实是2种设计思路,一种是合表存储,另外一种是分表存储。无论是分表还是合表存储,在面临海量数据时都会有很大的压力,那么我们该怎么面对呢?
MongoDB出现就是在解决海量数据存储问题的,那么MongoDB是如何解决的呢? 答案是:集群。
MongoDB有三种集群方式,分别是:主从集群、副本集群、分片式集群,其中主从集群官方已经不推荐了,一般用副本集群取代主从集群。
1.5.1、副本集群
一个主,两个从库组成,主库宕机时,这两个从库都可以被选为主库。
当主库宕机后,两个从库都会进行竞选,其中一个变为主库,当原主库恢复后,作为从库加入当前的复制集群即可。
1.5.2、分片集群
分片(sharding)是MongoDB用来将大型集合分割到不同服务器(或者说一个集群)上所采用的方法。
例如,如果数据库1tb的数据集,并有4个分片,然后每个分片可能仅持有256 GB的数据。如果有40个分片,那么每个切分可能只有25GB的数据。
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 镜像版本:
2.1、pojo
写到dubbo工程中:
package com.tanhua.dubbo.server.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.bson.types.ObjectId;import org.springframework.data.mongodb.core.mapping.Document;import java.util.Date;import java.util.List;/*** 此代码写到dubbo工程下的interface下* 发布表,动态内容*/@Data@NoArgsConstructor@AllArgsConstructor@Document(collection = "quanzi_publish")public class Publish implements java.io.Serializable {private static final long serialVersionUID = 8732308321082804771L;private ObjectId id; //主键idprivate Long userId;private String text; //文字private List<String> medias; //媒体数据,图片或小视频 urlprivate Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看private List<Long> seeList; //部分可见的列表private List<Long> notSeeList; //不给谁看的列表private String longitude; //经度private String latitude; //纬度private String locationName; //位置名称private Long created; //发布时间}
package com.tanhua.dubbo.server.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.bson.types.ObjectId;import org.springframework.data.mongodb.core.mapping.Document;import java.util.Date;/*** 相册表,用于存储自己发布的数据,每一个用户一张表进行存储*/@Data@NoArgsConstructor@AllArgsConstructor@Document(collection = "quanzi_album")public class Album implements java.io.Serializable {private static final long serialVersionUID = 432183095092216817L;private ObjectId id; //主键idprivate ObjectId publishId; //发布idprivate Long created; //发布时间}
package com.tanhua.dubbo.server.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.bson.types.ObjectId;import org.springframework.data.mongodb.core.mapping.Document;import java.util.Date;/*** 时间线表,用于存储发布(或推荐)的数据,每一个用户一张表进行存储*/@Data@NoArgsConstructor@AllArgsConstructor@Document(collection = "quanzi_time_line")public class TimeLine implements java.io.Serializable{private static final long serialVersionUID = 9096178416317502524L;private ObjectId id;private Long userId; // 好友idprivate ObjectId publishId; //发布idprivate Long date; //发布的时间}
package com.tanhua.dubbo.server.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.bson.types.ObjectId;import org.springframework.data.mongodb.core.mapping.Document;import java.util.Date;/*** 评论表*/@Data@NoArgsConstructor@AllArgsConstructor@Document(collection = "quanzi_comment")public class Comment implements java.io.Serializable{private static final long serialVersionUID = -291788258125767614L;private ObjectId id;private ObjectId publishId; //发布idprivate Integer commentType; //评论类型,1-点赞,2-评论,3-喜欢private String content; //评论内容private Long userId; //发布评论的评论人的idprivate Boolean isParent = false; //是否为父节点,默认是否private ObjectId parentId; // 父节点idprivate Long created; //发表时间}
package com.tanhua.dubbo.server.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.bson.types.ObjectId;import org.springframework.data.mongodb.core.mapping.Document;import java.util.Date;/*** 用户的好友列表映射的javabean类*/@Data@NoArgsConstructor@AllArgsConstructor@Document(collection = "tanhua_users")public class Users implements java.io.Serializable{private static final long serialVersionUID = 6003135946820874230L;private ObjectId id;private Long userId; //用户idprivate Long friendId; //好友idprivate Long date; //时间}
2.2、发布动态
2.2.1、定义接口
package com.tanhua.dubbo.server.api;import com.tanhua.dubbo.server.pojo.Publish;public interface QuanZiApi {/*** 发布动态的接口** @param publish 要发布的信息* @return 是否发布成功*/boolean savePublish(Publish publish);}
2.2.2、编写实现
在测试环境下构造好友数据(包名建议和接口的保持一致):
package com.tanhua.dubbo.server.api;import com.tanhua.dubbo.server.pojo.Users;import org.bson.types.ObjectId;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import java.util.Date;import java.util.List;@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class TestUsers {@Autowiredprivate MongoTemplate mongoTemplate;@Testpublic void saveUsers(){this.mongoTemplate.save(new Users(ObjectId.get(),1L, 2L, System.currentTimeMillis()));this.mongoTemplate.save(new Users(ObjectId.get(),1L, 3L, System.currentTimeMillis()));this.mongoTemplate.save(new Users(ObjectId.get(),1L, 4L, System.currentTimeMillis()));this.mongoTemplate.save(new Users(ObjectId.get(),1L, 5L, System.currentTimeMillis()));this.mongoTemplate.save(new Users(ObjectId.get(),1L, 6L, System.currentTimeMillis()));}@Testpublic void testQueryList(){Criteria criteria = Criteria.where("userId").is(1L);List<Users> users = this.mongoTemplate.find(Query.query(criteria), Users.class);for (Users user : users) {System.out.println(user);}}}
实现发布:
package com.tanhua.dubbo.server.api;import com.alibaba.dubbo.config.annotation.Service;import com.tanhua.dubbo.server.pojo.Album;import com.tanhua.dubbo.server.pojo.Publish;import com.tanhua.dubbo.server.pojo.TimeLine;import com.tanhua.dubbo.server.pojo.Users;import org.bson.types.ObjectId;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import java.util.Date;import java.util.List;@Service(version = "1.0.0")public class QuanZiApiImpl implements QuanZiApi {@Autowiredprivate MongoTemplate mongoTemplate;@Overridepublic boolean savePublish(Publish publish) {// 校验if (publish.getUserId() == null) {return false;}try {publish.setCreated(System.currentTimeMillis()); //设置创建时间publish.setId(ObjectId.get()); //设置idthis.mongoTemplate.save(publish); //保存发布Album album = new Album(); // 构建相册对象album.setPublishId(publish.getId());album.setCreated(System.currentTimeMillis());album.setId(ObjectId.get());this.mongoTemplate.save(album, "quanzi_album_" + publish.getUserId());//写入好友的时间线中Criteria criteria = Criteria.where("userId").is(publish.getUserId());List<Users> users = this.mongoTemplate.find(Query.query(criteria), Users.class);for (Users user : users) {TimeLine timeLine = new TimeLine();timeLine.setId(ObjectId.get());timeLine.setPublishId(publish.getId());//设置好友的id(相当于把自己的id写入到朋友时间线的好友id中)timeLine.setUserId(user.getUserId());timeLine.setDate(System.currentTimeMillis());this.mongoTemplate.save(timeLine, "quanzi_time_line_" + user.getFriendId());}return true;} catch (Exception e) {e.printStackTrace();//TODO 出错的事务回滚,MongoDB非集群不支持事务,暂不进行实现}return false;}}
测试用例:
@Testpublic void testSavePublish(){Publish publish = new Publish();publish.setUserId(1L);publish.setLocationName("上海市");publish.setSeeType(1);publish.setText("今天天气不错~");publish.setMedias(Arrays.asList("https://itcast-tanhua.oss-cn-shanghai.aliyuncs.com/images/quanzi/1.jpg"));boolean result = this.quanZiApi.savePublish(publish);System.out.println(result);}
2.2.3、编写接口服务
在服务工程中编写接口服务。
2.2.3.1、Controller
package com.tanhua.server.controller;import com.tanhua.server.service.MovementsService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;@RestController@RequestMapping("movements")public class MovementsController {@Autowiredprivate MovementsService movementsService;/*** 发送动态** @param textContent* @param location* @param multipartFile* @param token* @return*/@PostMapping()public ResponseEntity<Void> savePublish(@RequestParam(value = "textContent", required = false) String textContent,@RequestParam(value = "location", required = false) String location,@RequestParam(value = "latitude", required = false) String latitude,@RequestParam(value = "longitude", required = false) String longitude,@RequestParam(value = "imageContent", required = false) MultipartFile[] multipartFile,@RequestHeader("Authorization") String token) {try {boolean result = this.movementsService.savePublish(textContent, location,latitude, longitude, multipartFile, token);if(result){return ResponseEntity.ok(null);}} catch (Exception e) {e.printStackTrace();}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}
2.2.3.2、MovementsService
package com.tanhua.server.service;import com.alibaba.dubbo.config.annotation.Reference;import com.tanhua.common.pojo.User;import com.tanhua.server.service.PicUploadService;import com.tanhua.common.vo.PicUploadResult;import com.tanhua.dubbo.server.api.QuanZiApi;import com.tanhua.dubbo.server.pojo.Publish;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import java.util.ArrayList;import java.util.List;@Servicepublic class MovementsService {@Reference(version = "1.0.0")private QuanZiApi quanZiApi;@Autowiredprivate PicUploadService picUploadService;@Autowiredprivate UserService userService;public boolean savePublish(String textContent,String location,String latitude,String longitude,MultipartFile[] multipartFile,String token) {//查询当前的登录信息User user = this.userService.queryUserByToken(token);if (null == user) {return false;}Publish publish = new Publish();publish.setUserId(user.getId());publish.setText(textContent);publish.setLocationName(location);publish.setLatitude(latitude);publish.setLongitude(longitude);publish.setSeeType(1);List<String> picUrls = new ArrayList<>();//图片上传for (MultipartFile file : multipartFile) {PicUploadResult picUploadResult = this.picUploadService.upload(file);picUrls.add(picUploadResult.getName());}publish.setMedias(picUrls);return this.quanZiApi.savePublish(publish);}}
2.2.3.3、PicUploadService
导入所需依赖:
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>2.8.3</version></dependency><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency>
package com.tanhua.server.service;import com.aliyun.oss.OSSClient;import com.tanhua.server.config.AliyunConfig;import com.tanhua.server.vo.PicUploadResult;import org.apache.commons.lang3.RandomUtils;import org.apache.commons.lang3.StringUtils;import org.joda.time.DateTime;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;@Servicepublic class PicUploadService {// 允许上传的格式private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",".jpeg", ".gif", ".png"};@Autowiredprivate OSSClient ossClient;@Autowiredprivate AliyunConfig aliyunConfig;public PicUploadResult upload(MultipartFile uploadFile) {PicUploadResult fileUploadResult = new PicUploadResult();//图片做校验,对后缀名boolean isLegal = false;for (String type : IMAGE_TYPE) {if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(),type)) {isLegal = true;break;}}if (!isLegal) {fileUploadResult.setStatus("error");return fileUploadResult;}// 文件新路径String fileName = uploadFile.getOriginalFilename();String filePath = getFilePath(fileName);// 上传到阿里云try {// 目录结构:images/2018/12/29/xxxx.jpgossClient.putObject(aliyunConfig.getBucketName(), filePath, newByteArrayInputStream(uploadFile.getBytes()));} catch (Exception e) {e.printStackTrace();//上传失败fileUploadResult.setStatus("error");return fileUploadResult;}// 上传成功fileUploadResult.setStatus("done");fileUploadResult.setName(this.aliyunConfig.getUrlPrefix() + filePath);fileUploadResult.setUid(String.valueOf(System.currentTimeMillis()));return fileUploadResult;}private String getFilePath(String sourceFileName) {DateTime dateTime = new DateTime();return "images/" + dateTime.toString("yyyy")+ "/" + dateTime.toString("MM") + "/"+ dateTime.toString("dd") + "/" + System.currentTimeMillis() +RandomUtils.nextInt(100, 9999) + "." +StringUtils.substringAfterLast(sourceFileName, ".");}}
2.2.3.4、AliyunConfig
package com.tanhua.server.config;import com.aliyun.oss.OSSClient;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@PropertySource("classpath:aliyun.properties")@ConfigurationProperties(prefix = "aliyun")@Datapublic class AliyunConfig {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;private String urlPrefix;@Beanpublic OSSClient oSSClient() {return new OSSClient(endpoint, accessKeyId, accessKeySecret);}}
2.2.3.5、aliyun.properties
aliyun.endpoint = http://oss-cn-shanghai.aliyuncs.comaliyun.accessKeyId = xxxxxaliyun.accessKeySecret = xxxxaliyun.bucketName=itcast-tanhuaaliyun.urlPrefix=http://itcast-tanhua.oss-cn-shanghai.aliyuncs.com/
2.2.3.5、PicUploadResult
package com.tanhua.server.vo;import lombok.Data;@Datapublic class PicUploadResult {// 文件唯一标识private String uid;// 文件名private String name;// 状态有:uploading done error removedprivate String status;// 服务端响应内容,如:'{"status": "success"}'private String response;}
2.2.4、测试


结果:
2.2.5、整合测试


2.3、统一处理token
在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,接下来我们将使用拦截器+ThreadLocal的方式进行解决。
2.3.1、编写UserThreadLocal
package com.tanhua.server.utils;import com.tanhua.server.pojo.User;public class UserThreadLocal {private static final ThreadLocal<User> LOCAL = new ThreadLocal<User>();private UserThreadLocal() {}public static void set(User user) {LOCAL.set(user);}public static User get() {return LOCAL.get();}}
2.3.2、编写TokenInterceptor
package com.tanhua.server.interceptor;import com.tanhua.server.pojo.User;import com.tanhua.server.service.UserService;import com.tanhua.server.utils.NoAuthorization;import com.tanhua.server.utils.UserThreadLocal;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/*** 统一完成根据token查询用User的功能*/@Componentpublic class TokenInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;NoAuthorization noAnnotation = handlerMethod.getMethod().getAnnotation(NoAuthorization.class);if (noAnnotation != null) {// 如果该方法被标记为无需验证token,直接返回即可return true;}}String token = request.getHeader("Authorization");if (StringUtils.isNotEmpty(token)) {User user = this.userService.queryUserByToken(token);if (null != user) {UserThreadLocal.set(user); //将当前对象,存储到当前的线程中return true;}}//请求头中如不存在Authorization直接返回falseresponse.setStatus(401); //无权限访问return false;}}
2.3.3、编写注解NoAuthorization
package com.tanhua.server.utils;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented //标记注解public @interface NoAuthorization {}
2.3.4、注册拦截器
package com.tanhua.server.config;import com.tanhua.server.interceptor.RedisCacheInterceptor;import com.tanhua.server.interceptor.TokenInterceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class WebConfig implements WebMvcConfigurer {@Autowiredprivate RedisCacheInterceptor redisCacheInterceptor;@Autowiredprivate TokenInterceptor tokenInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注意拦截器的顺序registry.addInterceptor(this.tokenInterceptor).addPathPatterns("/**");registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");}}
2.3.5、使用ThreadLocal

2.4、查询好友动态
查询好友动态其实就是查询自己的时间线表,好友在发动态时已经将动态信息写入到了自己的时间线表中。
2.4.1、编写dubbo接口
package com.tanhua.dubbo.server.api;import com.tanhua.dubbo.server.pojo.Publish;import com.tanhua.dubbo.server.vo.PageInfo;public interface QuanZiApi {/*** 发布动态** @param publish* @return*/boolean savePublish(Publish publish);/*** 查询动态** @return*/PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize);}
2.4.2、编写实现
@Overridepublic PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize) {PageRequest pageRequest = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.desc("created")));Query query = new Query().with(pageRequest);//查询时间线表List<TimeLine> timeLineList = this.mongoTemplate.find(query, TimeLine.class, "quanzi_time_line_" + userId);List<ObjectId> publishIds = new ArrayList<>();for (TimeLine timeLine : timeLineList) {publishIds.add(timeLine.getPublishId());}//查询发布信息Query queryPublish = Query.query(Criteria.where("id").in(publishIds)).with(Sort.by(Sort.Order.desc("created")));List<Publish> publishList = this.mongoTemplate.find(queryPublish, Publish.class);PageInfo<Publish> pageInfo = new PageInfo<>();pageInfo.setPageNum(page);pageInfo.setPageSize(pageSize);pageInfo.setRecords(publishList);pageInfo.setTotal(0); //不提供总数return pageInfo;}
2.4.3、编写接口服务
在itcast-tanhua-server中完成。
/*** 查询好友动态** @param page* @param pageSize* @return*/@GetMappingpublic PageResult queryPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {return this.movementsService.queryPublishList(page, pageSize);}
2.4.4、编写movementsService
TODO的内容,在后面实现。
/*** 查询好友动态** @param page* @param pageSize* @return*/public PageResult queryPublishList(Integer page, Integer pageSize) {PageResult pageResult = new PageResult();//获取当前的登录信息User user = UserThreadLocal.get();PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(user.getId(), page, pageSize);pageResult.setPagesize(pageSize);pageResult.setPage(page);pageResult.setCounts(0);pageResult.setPages(0);List<Publish> records = pageInfo.getRecords();if (CollectionUtils.isEmpty(records)) {//没有动态信息return pageResult;}List<Movements> movementsList = new ArrayList<>();for (Publish record : records) {Movements movements = new Movements();//设置动态idmovements.setId(record.getId().toHexString());//设置图片信息movements.setImageContent(record.getMedias().toArray(new String[]{}));//设置文字信息movements.setTextContent(record.getText());//设置用户idmovements.setUserId(record.getUserId());//设置创建的时间信息movements.setCreateDate(RelativeDateFormat.format(new Date(record.getCreated())));movementsList.add(movements);}//创建集合存储用户idList<Long> userIds = new ArrayList<>();for (Movements movements : movementsList) {if(!userIds.contains(movements.getId())){userIds.add(movements.getUserId());}}QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();queryWrapper.in("user_id", userIds);//查询好友信息List<UserInfo> userInfos = this.userInfoService.queryList(queryWrapper);for (Movements movements : movementsList) {for (UserInfo userInfo : userInfos) {if (movements.getUserId().longValue() == userInfo.getUserId().longValue()) {movements.setAge(userInfo.getAge());movements.setAvatar(userInfo.getLogo());movements.setGender(userInfo.getSex().name().toLowerCase());movements.setNickname(userInfo.getNickName());movements.setTags(StringUtils.split(userInfo.getTags(), ','));movements.setCommentCount(10); //TODO 评论数movements.setDistance("1.2公里"); //TODO 距离movements.setHasLiked(1); //TODO 是否点赞(1是,0否)movements.setHasLoved(0); //TODO 是否喜欢(1是,0否)movements.setLikeCount(100); //TODO 点赞数movements.setLoveCount(80); //TODO 喜欢数break;}}}pageResult.setItems(movementsList);return pageResult;}
package com.tanhua.server.utils;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;public class RelativeDateFormat {//一分钟private static final long ONE_MINUTE = 60000L;//一小时private static final long ONE_HOUR = 3600000L;//一天private static final long ONE_DAY = 86400000L;//一周private static final long ONE_WEEK = 604800000L;private static final String ONE_SECOND_AGO = "秒前";private static final String ONE_MINUTE_AGO = "分钟前";private static final String ONE_HOUR_AGO = "小时前";private static final String ONE_DAY_AGO = "天前";private static final String ONE_MONTH_AGO = "月前";private static final String ONE_YEAR_AGO = "年前";public static void main(String[] args) throws ParseException {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:m:s");Date date = format.parse("2013-11-11 18:35:35");System.out.println(format(date));}public static String format(Date date) {//用当前时间减去之前创建的时间long delta = new Date().getTime() - date.getTime();if (delta < 1L * ONE_MINUTE) {long seconds = toSeconds(delta);return (seconds <= 0 ? 1 : seconds) + ONE_SECOND_AGO;}if (delta < 45L * ONE_MINUTE) {long minutes = toMinutes(delta);return (minutes <= 0 ? 1 : minutes) + ONE_MINUTE_AGO;}if (delta < 24L * ONE_HOUR) {long hours = toHours(delta);return (hours <= 0 ? 1 : hours) + ONE_HOUR_AGO;}if (delta < 48L * ONE_HOUR) {return "昨天";}if (delta < 30L * ONE_DAY) {long days = toDays(delta);return (days <= 0 ? 1 : days) + ONE_DAY_AGO;}if (delta < 12L * 4L * ONE_WEEK) {long months = toMonths(delta);return (months <= 0 ? 1 : months) + ONE_MONTH_AGO;} else {long years = toYears(delta);return (years <= 0 ? 1 : years) + ONE_YEAR_AGO;}}private static long toSeconds(long date) {return date / 1000L;}private static long toMinutes(long date) {return toSeconds(date) / 60L;}private static long toHours(long date) {return toMinutes(date) / 60L;}private static long toDays(long date) {return toHours(date) / 24L;}private static long toMonths(long date) {return toDays(date) / 30L;}private static long toYears(long date) {return toMonths(date) / 365L;}}
Movements对象:
package com.tanhua.server.vo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class Movements {private String id; //动态idprivate Long userId; //用户idprivate String avatar; //头像private String nickname; //昵称private String gender; //性别 man womanprivate Integer age; //年龄private String[] tags; //标签private String textContent; //文字动态private String[] imageContent; //图片动态private String distance; //距离private String createDate; //发布时间 如: 10分钟前private Integer likeCount; //点赞数private Integer commentCount; //评论数private Integer loveCount; //喜欢数private Integer hasLiked; //是否点赞(1是,0否)private Integer hasLoved; //是否喜欢(1是,0否)}
2.4.5、测试

2.5、查询推荐动态
推荐动态是通过推荐系统计算出的结果,现在我们只需要实现查询即可,推荐系统在后面的课程中完成。
推荐动态和好友动态的结构是一样的,所以我们只需要查询推荐的时间表即可。
2.5.1、修改dubbo服务逻辑 
2.5.2、编写测试用例
该测试用例用于插入推荐数据。
@Testpublic void testRecommendPublish(){//查询用户id为2的动态作为推荐动态的数据PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(2L, 1, 10);for (Publish record : pageInfo.getRecords()) {TimeLine timeLine = new TimeLine();timeLine.setId(ObjectId.get());timeLine.setPublishId(record.getId());timeLine.setUserId(record.getUserId());timeLine.setDate(System.currentTimeMillis());this.mongoTemplate.save(timeLine, "quanzi_time_line_recommend");}}
2.5.3、MovementsController
/*** 查询推荐动态** @param page* @param pageSize* @return*/@GetMapping("recommend")public PageResult queryRecommendPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {return this.movementsService.queryPublishList(page, pageSize, true);}
2.5.4、MovementsService

2.5.5、整合测试


