第5章 圈子功能实现-1
今日内容介绍
学习目标
- 完成公共列表-作业
- 圈子功能说明
- 圈子技术实现
- 圈子技术方案
- 圈子实现发布动态
- 圈子实现好友动态
- 圈子实现推荐动态
- 圈子实现点赞、喜欢功能(第6章实现)
- 圈子实现评论(第6章实现)
- 圈子实现评论的点赞(第6章实现)
1.公告管理-作业
【目标】
首页推荐需求介绍
首页推荐功能分析
首页推荐功能实现
【路径】
1:了解首页推荐需求
2:首页推荐功能分析
3:首页推荐功能实现
【讲解】
1.1. 服务消费者-公告管理
1.1.1. 接口说明
1.1.2. 公告实体类与VO
在tanhua-domain模块下创建Announcement
package com.tanhua.domain.db;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Announcement extends BasePojo {
private String id;
private String title;
private String description;
}
1.1.3. Announcement
在tanhua-domain模块下创建AnnouncementVo
package com.tanhua.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AnnouncementVo {
private String id;
private String title;
private String description;
private String createDate;
}
1.1.4. AnnounController
在tanhua-server模块下创建AnnounController
package com.tanhua.server.controller;
import com.tanhua.server.service.AnnounService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 公告
*/
@RestController
@RequestMapping("/messages")
public class AnnounController {
@Autowired
private AnnounService announService;
/**
* 查询公告列表
* GET /announcements
* 参数:page,pagesize
*/
@GetMapping("/announcements")
public ResponseEntity announcements(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pagesize) {
return announService.announcements(page,pagesize);
}
}
1.1.5. AnnounService
在tanhua-server模块下创建AnnounService
package com.tanhua.server.service;
import com.tanhua.domain.db.Announcement;
import com.tanhua.domain.vo.AnnouncementVo;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.dubbo.api.AnnouncementApi;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.BeanUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
@Service
public class AnnounService {
@Reference
private AnnouncementApi announcementApi;
/**
* 查询公告列表
*/
public ResponseEntity announcements(int page, int pagesize) {
//1、调用API查询分页数据 PageResult
PageResult<Announcement> pageResult = announcementApi.findAll(page, pagesize);
//2、获取所有的公告对象
List<Announcement> records = pageResult.getItems();
//3、一个公告对象,转化为一个vo对象
List<AnnouncementVo> list = new ArrayList<>();
for (Announcement record : records) {
AnnouncementVo vo = new AnnouncementVo();
BeanUtils.copyProperties(record,vo);
if(record.getCreated() != null) {
vo.setCreateDate(new SimpleDateFormat("yyyy-MM-dd hh:mm").format(record.getCreated()));
}
list.add(vo);
}
//4、构造需要的分页对象,PageResult
PageResult resultVo = new PageResult(pageResult.getCounts(), pageResult.getPagesize(),pageResult.getPages(),pageResult.getPage(),list);
//5、构造返回值
return ResponseEntity.ok(resultVo);
}
}
1.2. 服务提供者-公告管理
1.2.1. AnnouncementApi
在tanhua-dubbo-interface模块下创建AnnouncementApi
package com.tanhua.dubbo.api;
import com.tanhua.domain.db.Announcement;
import com.tanhua.domain.vo.PageResult;
public interface AnnouncementApi {
/**
* 分页查询
*/
PageResult<Announcement> findAll(int page, int size);
}
1.2.2 AnnouncementApiImpl
在tanhua-dubbo-service模块下创建AnnouncementApiImpl
package com.tanhua.dubbo.api;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tanhua.domain.db.Announcement;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.dubbo.mapper.AnnouncementMapper;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class AnnouncementApiImpl implements AnnouncementApi {
@Autowired
private AnnouncementMapper announcementMapper;
@Override
public PageResult<Announcement> findAll(int page, int size) {
Page<Announcement> pages = new Page<>(page,size);
IPage<Announcement> pageInfo = announcementMapper.selectPage(pages, new QueryWrapper<>());
PageResult<Announcement> pageResult = new PageResult<Announcement>(pageInfo.getTotal(), pageInfo.getSize(),pageInfo.getPages(),pageInfo.getCurrent(),pageInfo.getRecords());
return pageResult;
}
}
1.2.3. AnnouncementMapper
在tanhua-dubbo-service模块下创建AnnouncementMapper
package com.tanhua.dubbo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.domain.db.Announcement;
public interface AnnouncementMapper extends BaseMapper<Announcement> {
}
1.3.4. 测试
【小结】
掌握公共管理功能实现
2. 首页推荐
【目标】
首页推荐需求介绍
首页推荐功能分析
首页推荐功能实现
【路径】
1:了解首页推荐需求
2:首页推荐功能分析
3:首页推荐功能实现
【讲解】
2.3. 服务消费者-首页推荐
2.3.1. 接口说明
响应:
{
"counts": 4698,
"pagesize": 20,
"pages": 58,
"page": 16,
"items": [
{
"id": 1011,
"avatar": "assets/images/avatar_2.png",
"nickname": "黑马小妹",
"gender": "woman",
"age": 23,
"tags": [
"本科",
"年龄相仿",
"单身"
],
"fateValue": 96
},
{
"id": 2495,
"avatar": "assets/images/avatar_1.png",
"nickname": "米朵妹妹",
"gender": "man",
"age": 28,
"tags": [
"年龄相仿",
"本科",
"单身"
],
"fateValue": 87
}
......
]
}
2.3.2. RecommendUserQueryParam
在tanhua-domain模块vo下创建RecommendUserQueryParam
package com.tanhua.domain.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class RecommendUserQueryParam implements Serializable {
private Integer page;
private Integer pagesize;
private String gender;
private String lastLogin;
private Integer age;
private String city;
private String education;
}
2.3.3. TodayBestController
在tanhua-server模块TodayBestController创建recommendList
/**
* 推荐用户- 分页查询
*/
@RequestMapping(value = "/recommendation",method = RequestMethod.GET)
public ResponseEntity recommendation(RecommendUserQueryParam ruqp){
PageResult<TodayBestVo> pageResult = todayBestService.recommendation(ruqp.getPage(),ruqp.getPagesize());
return ResponseEntity.ok(pageResult);
}
2.3.4. TodayBestService
在tanhua-server模块TodayBestService创建recommendList
/**
* 今日佳人
*/
public TodayBestVo todayBest() {
Long toUserId = UserHolder.getUserId();//当前登录用户id
//1.查询mongodb获取今日佳人用户
RecommendUser recommendUser = recommendUserApi.queryWithMaxScore(toUserId);
//2.如果没有查询到今日佳人 设置默认推荐用户
if(recommendUser == null){
recommendUser = new RecommendUser();
recommendUser.setUserId(20l);//佳人用户id
recommendUser.setToUserId(toUserId);//当前用户id
recommendUser.setScore(99d);//缘分值
}
//3.查询推荐用户的用户信息tb_user_info
TodayBestVo vo = getTodayBestVo(recommendUser);
return vo;
}
/**
* 推荐用户-分页 分页查询
*/
public PageResult<TodayBestVo> recommendation(Integer page, Integer pagesize) {
Long userId = UserHolder.getUserId();//当前用户id
//1 分页查询推荐用户列表
PageResult<RecommendUser> recommendUserPageResult = recommendUserApi.findPageRecommendUser(userId,page,pagesize);
//2.如果分页列表数据为空 则设置默认分页数据(10条)
if(recommendUserPageResult == null || CollectionUtils.isEmpty(recommendUserPageResult.getItems())){
recommendUserPageResult = new PageResult<>(10l,10l,1l,1l,defaultRecommend());
}
//3 循环遍历 推荐用户列表 查询tb_userInfo
List<TodayBestVo> todayBestVoList = new ArrayList<>();
for (RecommendUser recommendUser : recommendUserPageResult.getItems()) {
TodayBestVo vo = getTodayBestVo(recommendUser);
todayBestVoList.add(vo);
}
//4.将数据封装PageResult<TodayBestVo>
PageResult<TodayBestVo> voPageResult = new PageResult<>();
BeanUtils.copyProperties(recommendUserPageResult,voPageResult); //copy counts pagesize pages page
voPageResult.setItems(todayBestVoList);//设置返回集合Vo
return voPageResult;
}
private TodayBestVo getTodayBestVo(RecommendUser recommendUser) {
//3.查询推荐用户的用户信息tb_user_info
UserInfo userInfo = userInfoApi.findUserInfoByUserId(recommendUser.getUserId());
//4.构建vo返回
TodayBestVo vo = new TodayBestVo();
BeanUtils.copyProperties(userInfo, vo);//nickname avatar gender age
if (!StringUtils.isEmpty(userInfo.getTags())) {
vo.setTags(userInfo.getTags().split(","));
}
vo.setId(recommendUser.getUserId());//佳人用户id
vo.setFateValue(recommendUser.getScore().longValue());//缘分值
return vo;
}
//构造默认数据
private List<RecommendUser> defaultRecommend() {
String ids = "1,2,3,4,5,6,7,8,9,10";
List<RecommendUser> records = new ArrayList<>();
for (String id : ids.split(",")) {
RecommendUser recommendUser = new RecommendUser();
recommendUser.setUserId(Long.valueOf(id));
recommendUser.setScore(RandomUtils.nextDouble(70, 98));
records.add(recommendUser);
}
return records;
}
2.2. 服务提供者-首页推荐
2.2.1. RecommendUserApi
在tanhua-dubbo-interface模块下创建RecommendUserApi
/**
* 首页推荐-分页列表查询
* @param userId
* @param page
* @param pagesize
* @return
*/
PageResult<RecommendUser> findPageRecommendUser(Long userId, Integer page, Integer pagesize);
2.2.2 RecommendUserApiImpl
在tanhua-dubbo-service模块下创建RecommendUserApiImpl
/**
* 首页推荐-分页列表查询
* @param userId
* @param page
* @param pagesize
* @return
*/
@Override
public PageResult<RecommendUser> findPageRecommendUser(Long userId, Integer page, Integer pagesize) {
Query query = new Query();
query.addCriteria(Criteria.where("toUserId").is(userId));
query.limit(pagesize);
query.skip((page-1)*pagesize);
long counts = mongoTemplate.count(query, RecommendUser.class);
//2,查询当前页面数据
List<RecommendUser> recommendUserList = mongoTemplate.find(query, RecommendUser.class);
//3.封装PageResult对象返回
//pages //总页数
long pages = counts/pagesize + (counts%pagesize > 0 ?1:0);
return new PageResult<>(counts,(long)pagesize,pages,(long)page,recommendUserList);
}
2.3.3. 测试
postman测试
app测试
【小结】
掌握首页推荐功能
3. 圈子功能
【目标】
圈子功能需求介绍
圈子功能功能分析
圈子功能功能实现
【路径】
1:了解圈子功能需求
2:圈子功能分析
3:圈子功能实现
【讲解】
3.1. 功能介绍
探花交友项目中的圈子功能,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。
发布:
3.2. 实现方案分析
对于圈子功能的实现,我们需要对它的功能特点做分析:
- 数据量会随着用户数增大而增大
- 读多写少
- 非好友看不到其动态内容
- ……
针对以上特点,我们来分析一下:
- 对于数据量大而言,显然不能够使用关系型数据库进行存储,我们需要通过MongoDB进行存储
- 对于读多写少的应用,需要减少读取的成本
- 比如说,一条SQL语句,单张表查询一定比多张表查询要快
- 对于每个人数据在存储层面最好做到相互隔离,这样的话就不会有影响
所以对于存储而言,主要是核心的6张表:
- 发布表:记录了所有用户的发布的东西信息,如图片、视频等。
- 相册:相册是每个用户独立的,记录了该用户所发布的所有内容。
- 评论:针对某个具体发布的朋友评论和点赞操作。
- 时间线:所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。
- 好友表:记录好友关系
- 推荐圈子表:记录推荐动态表
3.3. 技术方案(重点)
根据之前我们的分析,对于技术方案而言,将采用MongoDB+Redis来实现,其中MongoDB负责存储,Redis负责缓存数据。
3.1.1. 发布流程
流程说明:
- 用户发布动态,首先将动态内容写入到发布表。
- 然后,将发布的指向写入到自己的相册表中。
- 最后,将发布的指向写入到好友的时间线中。
3.1.2. 查看流程
流程说明:
- 用户查看动态,如果查看自己的动态,直接查询相册表即可
- 如果查看好友动态,查询时间线表即可
- 如果查看推荐动态,查看推荐表即可
由此可见,查看动态的成本较低,可以快速的查询到动态数据。
3.4. 数据库表分析-重点理解
数据库表使用分析-重点掌握
为什么这么设计表?-了解
****为了提升查询速度,因为查询频率远远高于写入频率,所以设计时间线表 相册表,这2张表数据远远小于发布表。直接在这2个表中做分页查询速度比较快的,查询完成后得到发布id,再根据发布id查询发布表,得到动态数据。
3.4.1. 数据库表
① 发布表
#表名: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
}
⑤ 好友表
#表名:tanhua_users
{
"id":1, #主键id
"userId":1001, #用户id
"friendId":1, #好友id
"created":1568012791171
}
⑥ 推荐动态表
#表名:recommend_quanzi
{
"id" : 1, #主键id
"userId" : 1001, #用户id
"score" : 9.0,
"created" : 1568012791171,
"publishId":1001, #发布id
}
3.4.2. 实体类与VO
tanhua-domain模块mongo包中创建以下实体对象
① Publish
package com.tanhua.domain.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.List;
/**
* <p>
* 发布的动态信息表
* </p>
*/
@Data
@Document(collection = "quanzi_publish")
public class Publish implements Serializable {
private ObjectId id; //主键id
private Long pid; //Long类型,用于推荐系统的模型
private Long userId;
private String textContent; //文字
private List<String> medias; //媒体数据,图片或小视频 url
private Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
private String longitude; //经度
private String latitude; //纬度
private String locationName; //位置名称
private Long created; //发布时间
private Integer likeCount=0; //点赞数
private Integer commentCount=0; //评论数
private Integer loveCount=0; //喜欢数
}
② Album
package com.tanhua.domain.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
/**
* <p>
* 相册表,用于存储自己发布的数据,每一个用户一张表进行存储
* </p>
*/
@Data
public class Album implements Serializable {
private ObjectId id; //主键id
private ObjectId publishId; //发布id
private Long created; //发布时间
}
③ TimeLine
package com.tanhua.domain.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
/**
* <p>
* 时间线表,用于存储发布(或推荐)的数据,每一个用户一张表进行存储
* </p>
*/
@Data
public class TimeLine implements Serializable {
private ObjectId id;
private Long userId; // 好友id
private ObjectId publishId; //发布id
private Long created; //发布的时间
}
④ Friend
package com.tanhua.domain.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
/**
* <p>
* 好友关系
* </p>
*/
@Data
@Document(collection = "tanhua_users")
public class Friend implements Serializable {
private ObjectId id;
private Long userId; //用户id
private Long friendId; //好友id
private Long created; //时间
}
⑤ RecommendQuanzi
package com.tanhua.domain.mongo;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
/**
* <p>
* 推荐动态
* </p>
*/
@Data
@Document(collection = "recommend_quanzi")
public class RecommendQuanzi implements Serializable {
@Id
private ObjectId id; // 主键
@Indexed
private Long userId; // 推荐的用户id
private Long pid;
private ObjectId publishId; // 发布的动态的id
@Indexed
private Double score = 0d; // 推荐分数
private Long created; // 日期
}
tanhua-domain模块vo包中创建以下VO
⑥ PublishVo
package com.tanhua.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class PublishVo implements Serializable {
private Long userId; // 用户id
private String textContent; // 文本内容
private String location; // 地理位置
private String longitude; // 经度
private String latitude; // 纬度
private List<String> medias; // 图片url
}
⑦ MomentVo
package com.tanhua.domain.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class MomentVo implements Serializable {
private String id; //动态id
private Long userId; //用户id
private String avatar; //头像
private String nickname; //昵称
private String gender; //性别 man woman
private Integer age; //年龄
private String[] tags; //标签
private String textContent; //文字动态
private String[] imageContent; //图片动态
private String distance; //距离
private String createDate; //发布时间 如: 10分钟前
private int likeCount; //点赞数
private int commentCount; //评论数
private int loveCount; //喜欢数
private Integer hasLiked; //是否点赞(1是,0否)
private Integer hasLoved; //是否喜欢(1是,0否)
}
3.5. 服务消费者-发布动态
3.5.1. 接口说明
3.5.2. MomentController
tanhua-server模块编写MomentController,完成发布动态功能
package com.tanhua.server.controller;
import com.tanhua.domain.vo.PublishVo;
import com.tanhua.server.service.MovementsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 圈子管理-控制层
*/
@RestController
@RequestMapping("/movements")
public class MovementsController {
@Autowired
private MovementsService movementsService;
/**
* 发布动态
*/
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity savePublish(PublishVo publishVo, MultipartFile[] imageContent) throws IOException {
movementsService.savePublish(publishVo,imageContent);
return ResponseEntity.ok(null);
}
}
3.5.3. MovementsService
tanhua-server模块
编写MovementsService,完成发布动态功能
package com.tanhua.server.service;
import com.tanhua.commons.templates.OssTemplate;
import com.tanhua.domain.vo.PublishVo;
import com.tanhua.dubbo.api.mongo.PublishApi;
import com.tanhua.server.interceptor.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 圈子管理-业务逻辑处理层
*/
@Service
@Transactional
@Slf4j //日志注解
public class MovementsService {
@Reference
private PublishApi publishApi;
@Autowired
private OssTemplate ossTemplate;
/**
* 发布动态
*/
public void savePublish(PublishVo publishVo, MultipartFile[] imageContent) throws IOException {
Long userId = UserHolder.getUserId();
// 1.封装PublishVo 用户id
publishVo.setUserId(userId);//当前用户id
//2 图片地址
List<String> medias = new ArrayList<>();
if(imageContent != null && imageContent.length>0) {
for (MultipartFile multipartFile : imageContent) {
String imgUrl = ossTemplate.upload(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
medias.add(imgUrl);
}
}
publishVo.setMedias(medias);//图片地址
//3. 调用服务将publishVo传入
publishApi.savePublish(publishVo);
}
}
3.6. 服务提供者-发布动态
3.6.1. PublishApi
tanhua-dubbo-interface模块mongo包中创建PublishApi
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.vo.PublishVo;
/**
* 圈子服务接口
*/
public interface PublishApi {
/**
* 发布动态
* @param publishVo
*/
void savePublish(PublishVo publishVo);
}
3.6.2. PublishApiImpl
tanhua-dubbo-service模块mongo包中创建PublishApiImpl
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.mongo.Album;
import com.tanhua.domain.mongo.Friend;
import com.tanhua.domain.mongo.Publish;
import com.tanhua.domain.mongo.TimeLine;
import com.tanhua.domain.vo.PublishVo;
import org.apache.dubbo.config.annotation.Service;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
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 org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 圈子服务接口实现类
*/
@Service
public class PublishApiImpl implements PublishApi{
@Autowired
private MongoTemplate mongoTemplate;
/**
* 发布动态
* @param publishVo
*/
@Override
public void savePublish(PublishVo publishVo) {
Long userId = publishVo.getUserId();///当前用户id
long nowTime = System.currentTimeMillis();
//1.往发布表插入动态数据
Publish publish = new Publish();
BeanUtils.copyProperties(publishVo,publish);//用户id 文本内容 经纬度 图片URL
publish.setLocationName(publishVo.getLocation());//地址位置
publish.setId(ObjectId.get());//设置主键id 后续相册表 时间线表 都跟此id 产生关系
publish.setPid(66l);//推荐系统 忽略此字段即可
publish.setSeeType(1);//谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
publish.setCreated(nowTime);//发布时间
mongoTemplate.insert(publish);
//2.往相册表插入我的动态记录
Album album = new Album();
album.setId(ObjectId.get());//主键id
album.setPublishId(publish.getId());//发布id
album.setCreated(nowTime);//当前时间
mongoTemplate.insert(album,"quanzi_album_"+userId);
//3,查询好友表 得到好友ids
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId));
List<Friend> friendList = mongoTemplate.find(query, Friend.class);
//4.根据好友ids往好友时间线表插入数据
if(!CollectionUtils.isEmpty(friendList)){
for (Friend friend : friendList) {
TimeLine timeLine = new TimeLine();
timeLine.setId(ObjectId.get());//主键id
timeLine.setUserId(userId);//好友id
timeLine.setPublishId(publish.getId());//发布id
timeLine.setCreated(nowTime);//创建时间
mongoTemplate.insert(timeLine,"quanzi_time_line_"+friend.getFriendId());
}
}
}
}
3.6.3. 测试
postman测试
结果:
app测试
3.7. 服务消费者-查询好友动态
查询好友动态其实就是查询自己的时间线表,好友在发动态时已经将动态信息写入到了自己的时间线表中。
3.7.1. MomentController
tanhua-server模块controller包中修改 MomentController 完成查询好友动态功能
/**
* 好友动态
*/
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity findPagePublish(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pagesize) {
PageResult<MomentVo> pageResult = movementsService.findPagePublish(page,pagesize);
return ResponseEntity.ok(pageResult);
}
3.7.2. MomentService
tanhua-server模块service包中修改 MomentService 完成查询好友动态功能
/**
* 好友动态
*/
public PageResult<MomentVo> findPagePublish(int page, int pagesize) {
Long userId = UserHolder.getUserId();//当前用户id
//1调用服务 分页查询发布表数据
PageResult<Publish> pagePublish =publishApi.findPagePublish(userId,page,pagesize);
if(pagePublish == null || CollectionUtils.isEmpty(pagePublish.getItems())){
return new PageResult<>(0l,10l,0l,1l,null);
//return null;
}
//2根据用户id查询用户信息
//2.1将publish userInfo 转为 MomentVo
List<MomentVo> momentVoList = new ArrayList<>();
for (Publish publish : pagePublish.getItems()) {
MomentVo momentVo = new MomentVo();
Long publishUserId = publish.getUserId();//动态发布者用户id
//用户信息
UserInfo userInfo = userInfoApi.findUserInfoByUserId(publishUserId);
//copy
BeanUtils.copyProperties(publish,momentVo);//userId textContent likeCount commentCount loveCount
BeanUtils.copyProperties(userInfo,momentVo);// nickname avatar gender age
//设置动态id
momentVo.setId(publish.getId().toHexString());//动态id
//设置标签
if(!StringUtils.isEmpty(userInfo.getTags())){
momentVo.setTags(userInfo.getTags().split(","));
}
//图片url地址 将List<String> medias 转为 String[] imageContent
momentVo.setImageContent(publish.getMedias().toArray(new String[]{}));
momentVo.setDistance("1米");//距离
//发布时间
momentVo.setCreateDate(RelativeDateFormat.format(new Date(publish.getCreated())));
//是否点赞 是否喜欢
momentVo.setHasLiked(0);
momentVo.setHasLoved(0);
momentVoList.add(momentVo);
}
//3封装数据返回
PageResult<MomentVo> voPageResult = new PageResult<>();
BeanUtils.copyProperties(pagePublish,voPageResult);
voPageResult.setItems(momentVoList);
return voPageResult;
}
3.7.3. RelativeDateFormat
tanhua-server模块utils包中创建RelativeDateFormat
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;
}
}
3.8. 服务提供者-查询好友动态
3.8.1. PublishApi
tanhua-dubbo-interface模块mongo包中创建PublishApi
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.domain.vo.PublishVo;
/**
* <p>
* 圈子动态api
* </p>
*/
public interface PublishApi {
/**
* 好友动态分页查询
* @param userId
* @param page
* @param pagesize
* @return
*/
PageResult<Publish> findPagePublish(Long userId, int page, int pagesize);
}
3.8.2. PublishApiImpl
tanhua-dubbo-service模块mongo包中创建PublishApiImpl
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.mongo.*;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.domain.vo.PublishVo;
import com.tanhua.dubbo.utils.IdService;
import org.apache.dubbo.config.annotation.Service;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
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.List;
import java.util.stream.Collectors;
/**
* <p>
* 圈子动态api的实现,也为其服务的提供者
* </p>
*/
@Service
public class PublishApiImpl implements PublishApi {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 好友动态
* @param userId
* @param page
* @param pagesize
* @return
*/
@Override
public PageResult<Publish> findPagePublish(Long userId, int page, int pagesize) {
//1查询好友时间线表(分页查询)
Query query = new Query();
query.limit(pagesize);
query.skip((page-1)*pagesize);
query.with(Sort.by(Sort.Direction.DESC,"created"));
long counts = mongoTemplate.count(query, "quanzi_time_line_"+userId);
//1.1,查询当前页面数据
List<TimeLine> timeLineList = mongoTemplate.find(query,TimeLine.class, "quanzi_time_line_"+userId);
//2再查询发布表动态数据
if(CollectionUtils.isEmpty(timeLineList)){
return new PageResult<>(counts,(long)pagesize,0l,(long)page,null);
}
//将List<TimeLine> 转为 List<Publish>
List<Publish> publishList = new ArrayList<>();
for (TimeLine timeLine : timeLineList) {
//2.1根据发布id 查询 发布表
ObjectId publishId = timeLine.getPublishId();//发布id
Publish publish = mongoTemplate.findById(publishId, Publish.class);
if(publish != null) {
publishList.add(publish);
}
}
//3返回PageResult<Publish>分页数据
long pages = counts/pagesize + (counts%pagesize > 0 ?1:0);
return new PageResult<>(counts,(long)pagesize,pages,(long)page,publishList);
}
}
3.8.3. 测试
3.9. 服务消费者-查询推荐动态
推荐动态是通过推荐系统计算出的结果,现在我们只需要实现查询即可。
推荐动态和好友动态的结构是一样的,所以我们只需要查询推荐的时间表即可。
3.9.1. MomentController
tanhua-server模块controller包中修改MomentController 完成查询推荐动态功能
/**
* 推荐动态-陌生人动态
*/
@RequestMapping(value = "/recommend",method = RequestMethod.GET)
public ResponseEntity findPageRecommend(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pagesize) {
PageResult<MomentVo> pageResult = movementsService.findPageRecommend(page,pagesize);
return ResponseEntity.ok(pageResult);
}
3.9.2. MomentService
tanhua-server模块service包中修改MomentService完成查询推荐动态功能
/**
* 好友动态
*/
public PageResult<MomentVo> findPagePublish(int page, int pagesize) {
Long userId = UserHolder.getUserId();//当前用户id
//1调用服务 分页查询发布表数据
PageResult<Publish> pagePublish =publishApi.findPagePublish(userId,page,pagesize);
if(pagePublish == null || CollectionUtils.isEmpty(pagePublish.getItems())){
return new PageResult<>(0l,10l,0l,1l,null);
}
return getMomentVoPageResult(pagePublish);
}
/**
* 将publish userInfo 转为 MomentVo 动态数据转换公共方法
* @param pagePublish
* @return
*/
private PageResult<MomentVo> getMomentVoPageResult(PageResult<Publish> pagePublish) {
//2根据用户id查询用户信息
//2.1将publish userInfo 转为 MomentVo
List<MomentVo> momentVoList = new ArrayList<>();
for (Publish publish : pagePublish.getItems()) {
MomentVo momentVo = new MomentVo();
Long publishUserId = publish.getUserId();//动态发布者用户id
//用户信息
UserInfo userInfo = userInfoApi.findUserInfoByUserId(publishUserId);
//copy
BeanUtils.copyProperties(publish,momentVo);//userId textContent likeCount commentCount loveCount
BeanUtils.copyProperties(userInfo,momentVo);// nickname avatar gender age
//设置动态id
momentVo.setId(publish.getId().toHexString());//动态id
//设置标签
if(!StringUtils.isEmpty(userInfo.getTags())){
momentVo.setTags(userInfo.getTags().split(","));
}
//图片url地址 将List<String> medias 转为 String[] imageContent
momentVo.setImageContent(publish.getMedias().toArray(new String[]{}));
momentVo.setDistance("1米");//距离
//发布时间
momentVo.setCreateDate(RelativeDateFormat.format(new Date(publish.getCreated())));
//是否点赞 是否喜欢
momentVo.setHasLiked(0);
momentVo.setHasLoved(0);
momentVoList.add(momentVo);
}
//3封装数据返回
PageResult<MomentVo> voPageResult = new PageResult<>();
BeanUtils.copyProperties(pagePublish,voPageResult);
voPageResult.setItems(momentVoList);
return voPageResult;
}
/**
* 推荐动态-陌生人动态
*/
public PageResult<MomentVo> findPageRecommend(int page, int pagesize) {
Long userId = UserHolder.getUserId();//当前用户id
//1调用服务 分页查询发布表数据
PageResult<Publish> pagePublish =publishApi.findPageRecommend(userId,page,pagesize);
if(pagePublish == null || CollectionUtils.isEmpty(pagePublish.getItems())){
return new PageResult<>(0l,10l,0l,1l,null);
}
return getMomentVoPageResult(pagePublish);
}
3.10. 服务提供者-查询推荐动态
3.10.1. PublishApi
tanhua-dubbo-interface模块mongo包中创建PublishApi
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.domain.vo.PublishVo;
/**
* <p>
* 圈子动态api
* </p>
*/
public interface PublishApi {
/**
* 推荐动态-陌生人动态
*/
PageResult<Publish> findPageRecommend(Long userId, int page, int pagesize);
}
3.10.2. PublishApiImpl
tanhua-dubbo-service模块mongo包中创建PublishApiImpl
package com.tanhua.dubbo.api.mongo;
import com.tanhua.domain.mongo.*;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.domain.vo.PublishVo;
import com.tanhua.dubbo.utils.IdService;
import org.apache.dubbo.config.annotation.Service;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
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.List;
import java.util.stream.Collectors;
/**
* <p>
* 圈子动态api的实现,也为其服务的提供者
* </p>
*/
@Service
public class PublishApiImpl implements PublishApi {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 推荐动态-陌生人动态
*/
@Override
public PageResult<Publish> findPageRecommend(Long userId, int page, int pagesize) {
//1查询推荐动态表(分页查询)
Query query = new Query();
query.limit(pagesize);
query.skip((page-1)*pagesize);
query.addCriteria(Criteria.where("userId").is(userId));
query.with(Sort.by(Sort.Direction.DESC,"created"));
long counts = mongoTemplate.count(query, RecommendQuanzi.class);
//1.1,查询当前页面数据
List<RecommendQuanzi> recommendQuanziList = mongoTemplate.find(query,RecommendQuanzi.class);
//2再查询发布表动态数据
if(CollectionUtils.isEmpty(recommendQuanziList)){
return new PageResult<>(counts,(long)pagesize,0l,(long)page,null);
}
//将List<RecommendQuanzi> 转为 List<Publish>
List<Publish> publishList = new ArrayList<>();
for (RecommendQuanzi recommendQuanzi : recommendQuanziList) {
//2.1根据发布id 查询 发布表
ObjectId publishId = recommendQuanzi.getPublishId();//发布id
Publish publish = mongoTemplate.findById(publishId, Publish.class);
if(publish != null) {
publishList.add(publish);
}
}
//3返回PageResult<Publish>分页数据
long pages = counts/pagesize + (counts%pagesize > 0 ?1:0);
return new PageResult<>(counts,(long)pagesize,pages,(long)page,publishList);
}
}
3.10.3. 测试
1.将publishId为空的数据删除
db.recommend_quanzi.remove({"publishId":null})
2.查询推荐动态数据
db.recommend_quanzi.find({userId:1}).sort({created:-1}).limit(10).skip(0);
手动修改publishId(id全部重复了)
【小结】
掌握圈子功能实现
4. 缓存-作业
【目标】
掌握缓存使用
【路径】
1:Spring Cache简介
2:Spring Cache使用
【讲解】
实现缓存逻辑有2种方式:
- 每个接口单独控制缓存逻辑
- 统一控制缓存逻辑
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;
-
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
-
Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache ,ConcurrentMapCache等;
-
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
-
使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
4.1. 重要概念
名称 | 解释 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 与@Cacheable区别在于是否每次都调用方法,常用于更新 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@CacheConfig | 统一配置本类的缓存注解的属性 |
4.2. SPEL表达式
Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodname |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
注意:
1.当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。 如
@Cacheable(key = "targetClass + methodName +#p0")
2.使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。 如:
@Cacheable(value="users", key="#id")
@Cacheable(value="users", key="#p0")
SpEL提供了多种运算符
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&,||,!,and,or,not,between,instanceof |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],[1],$[…] |
以上的知识点适合你遗忘的时候来查阅,下面正式进入学习!
4.3. 入门案例
4.3.1. 导入依赖
略
4.3.2. 开启缓存支持
然后在启动类注解@EnableCaching开启缓存
@SpringBootApplication
@EnableCaching //开启缓存
public class DemoApplication{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4.3.3. 缓存@Cacheable
@Cacheable
注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。
@Cacheable(value = "user" ,key = "#userId")
public User queryById(Long userId) {
UserInfo user = new UserInfo();
user.setId(userId);
user.setNickname("ceshi");
return user;
}
此处的value
是必需的,它指定了你的缓存存放在哪块命名空间。
此处的key
是使用的spEL表达式,参考上章。这里有一个小坑,如果你把methodName
换成method
运行会报错,观察它们的返回类型,原因在于methodName
是String
而methoh
是Method
。
此处的User
实体类一定要实现序列化public class User implements Serializable
,否则会报java.io.NotSerializableException
异常。
到这里,你已经可以运行程序检验缓存功能是否实现。
4.3.4. 更新@CachePut
@CachePut
注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable
不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。但需要注意的是该注解的value
和 key
必须与要更新的缓存相同,也就是与@Cacheable
相同。示例:
@CachePut(value = "user", key = "#userId")
public User updata(Long userId) {
UserInfo user = new UserInfo();
user.setId(userId);
user.setNickname("ceshi123");
return user;
}
4.3.5. 清除@CacheEvict
@CachEvict
的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。
//清除一条缓存,key为要清空的数据
@CacheEvict(value="user",key="#id")
public void delect(int id) {
//删除用户
}
【小结】
掌握Spring Cache缓存使用
作业
1.公共管理-作业
2.首页推荐-功能思路分析
3.发布动态-功能思路分析
4.好友动态-功能思路分析
5.推荐动态-功能思路分析
6.缓存
总结
1.首页推荐
页面发送首页推荐请求>controller(看接口文档)>service(业务处理 调用服务获取recommend_user 查询tb_user_info)==>将tb_user_info recommend数据封装vo返回
2.发布动态
点击发布动态发送请求>controller(看接口文档 接收两个参数)>service(业务处理将图片转为地址设置到vo 调用服务)==>a.往发布表插入动态数据 b.往相册表插入我的动态记录 c.查询好友表 得到好友ids d.根据好友ids往好友时间线表插入数据
3.好友动态
点击好友动态发送请求>controller(看接口文档 接收两个分页参数)>service(业务处理 调用服务获取好友动态数据 将动态数据 userInfo转为vo进行返回)==>a.查询好友时间表 b.查询发布表
4.推荐动态
点击好友动态发送请求>controller(看接口文档 接收两个分页参数)>service(业务处理 调用服务获取推荐动态数据 将动态数据 userInfo转为vo进行返回)==>a.查询推荐动态表 b.查询发布表
… ↩︎