实现功能
- 圈子动态查询
- 圈子实现评论
- 圈子实现点赞、喜欢功能
- 圈子实现评论
1、动态查询
我的动态:查询个人发布的动态列表(分页查询),和之前实现的好友动态,推荐动态实现逻辑是一致。
1.1、查询好友动态
查询好友动态与查询推荐动态显示的结构是一样的,只是其查询数据源不同
1.1.1、接口文档
API接口文档:http://192.168.136.160:3000/project/19/interface/api/142

1.1.2、代码步骤
Controller层接受请求参数
Service数据封装
- 调用API查询好友动态详情数据
- 调用API查询动态发布人详情
- 构造VO对象
API层根据用户ID查询好友发布动态详情

1.1.3、代码实现
MovementController
1 2 3 4 5 6 7 8 9 10
|
@GetMapping public ResponseEntity movements(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult pr = movementService.findFriendMovements(page,pagesize); return ResponseEntity.ok(pr); }
|
MovementService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public PageResult findFriendMovements(Integer page, Integer pagesize) { Long userId = UserHolder.getUserId(); List<Movement> items = movementApi.findFriendMovements(userId,page,pagesize); if(CollUtil.isEmpty(items)) { return new PageResult(); } List<Long> userIds = CollUtil.getFieldValues(items, "userId", Long.class); Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : items) { UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } return new PageResult(page,pagesize,0L,vos); }
|
movementApi
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override public List<Movement> findFriendMovements(Long friendId, Integer page, Integer pagesize) { Query query = Query.query(Criteria.where("friendId").in(friendId)) .skip((page - 1)*pagesize).limit(pagesize) .with(Sort.by(Sort.Order.desc("created"))); List<MovementTimeLine> lines = mongoTemplate.find(query, MovementTimeLine.class); List<ObjectId> movementIds = CollUtil.getFieldValues(lines, "movementId", ObjectId.class); Query movementQuery = Query.query(Criteria.where("id").in(movementIds)); return mongoTemplate.find(movementQuery, Movement.class); }
|
1.2、查询推荐动态
推荐动态是通过推荐系统计算出的结果,根据个人的偏好进行实时计算得出的结果
推荐系统采集用户的行为特征(日常操作行为)
大数据推荐系统实时计算,统计推荐数据
将结果写入redis
查询推荐动态

推荐动态-数据格式
- 推荐动态数据存入Redis服务器中
- 存入的数据KEY为MOVEMENTS_RECOMMED-{用户id}
- 存入的数据VALUE为:动态PID字符串(多个PID间使用”,”连接)
1 2
| 192.168.31.81:6379> get MOVEMENTS_RECOMMEND_1 "2562,3639,2063,3448,2128,2597,2893,2333,3330,2642,2541,3002,3561,3649,2384,2504,3397,2843,2341,2249"
|
可以看到,在Redis中的数据是有多个发布id组成(pid)由逗号分隔。所以实现中需要自己对这些数据做分页处理。
1.2.1、接口文档
API接口文档:http://192.168.136.160:3000/project/19/interface/api/145
1.2.2、代码步骤
Controller层接受请求参数
Service数据封装
- 从redis获取当前用户的推荐PID列表
- 如果不存在,调用API随机获取10条动态数据
- 如果存在,调用API根据PID列表查询动态数据
- 构造VO对象
API层编写方法

1.2.3、代码实现
Constants
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package com.tanhua.commons.utils;
public class Constants {
public static final String SMS_CODE = "CHECK_CODE_";
public static final String MOVEMENTS_RECOMMEND = "MOVEMENTS_RECOMMEND_";
public static final String VIDEOS_RECOMMEND = "VIDEOS_RECOMMEND_";
public static final String MOVEMENTS_INTERACT_KEY = "MOVEMENTS_INTERACT_";
public static final String MOVEMENT_LIKE_HASHKEY = "MOVEMENT_LIKE_";
public static final String MOVEMENT_LOVE_HASHKEY = "MOVEMENT_LOVE_";
public static final String VIDEO_LIKE_HASHKEY = "VIDEO_LIKE";
public static final String VISITORS = "VISITORS";
public static final String FOCUS_USER = "FOCUS_USER_{}_{}";
public static final String INIT_PASSWORD = "123456";
public static final String HX_USER_PREFIX = "hx";
public static final String JWT_SECRET = "itcast";
public static final int JWT_TIME_OUT = 3_600; }
|
MovementController
1 2 3 4 5 6 7 8 9
|
@GetMapping("/recommend") public ResponseEntity recommend(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult pr = movementService.findRecommendMovements(page,pagesize); return ResponseEntity.ok(pr); }
|
MovementService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public PageResult findRecommendMovements(Integer page, Integer pagesize) { String redisKey = "MOVEMENTS_RECOMMEND_" + UserHolder.getUserId(); String redisData = this.redisTemplate.opsForValue().get(redisKey); List<Movement> list = Collections.EMPTY_LIST; if(StringUtils.isEmpty(redisData)){ list = movementApi.randomMovements(pagesize); }else { String[] split = redisData.split(","); if ((page-1) * pagesize > split.length) { return new PageResult(); } List<Long> pids = Arrays.stream(split) .skip((page - 1) * pagesize) .limit(pagesize) .map(e -> Convert.toLong(e)) .collect(Collectors.toList()); list = movementApi.findByPids(pids); } List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class); Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : list) { UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } return new PageResult(page,pagesize,0L,vos); }
|
movementApi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public List<Movement> randomMovements(Integer counts) {
TypedAggregation aggregation = Aggregation.newAggregation(Movement.class, Aggregation.sample(counts));
AggregationResults<Movement> movements = mongoTemplate.aggregate(aggregation,Movement.class);
return movements.getMappedResults(); }
public List<Movement> findByPids(List<Long> pids) { Query query = Query.query(Criteria.where("pId").in(pids)); return mongoTemplate.find(query, Movement.class); }
|
1、推荐系统动态的执行流程
- 推荐系统采集用户行为数据
- 实时计算推荐数据写入redis
- 从redis查询推荐结果
2、基于List实现分页
- Stream流处理
- Skip()和limit()方法
3、从MongoDB获取随机数
- Aggregation.sample()设置随机采样数
- aggregate()方法统计
1.3、根据id查询动态详情

根据id查询动态:当手机端查看评论内容时(需要根据动态id,查询动态详情),后续再去查询评论列表

1.3.1、接口文档
API接口文档:http://192.168.136.160:3000/project/19/interface/api/151
1.3.2、代码实现
MovementController
1 2 3 4 5 6 7 8 9
|
@GetMapping("/{id}") public ResponseEntity findById(@PathVariable("id") String movementId) { MovementsVo vo = movementService.findMovementById(movementId); return ResponseEntity.ok(vo); }
|
MovementService
1 2 3 4 5 6 7 8 9 10
| public MovementsVo findMovementById(String movementId) { Movement movements = movementApi.findById(movementId); if(movements == null) { return null; }else { UserInfo userInfo = userInfoApi.findById(movements.getUserId()); return MovementsVo.init(userInfo,movements); } }
|
movementApi
1 2 3 4
| @Override public Movement findById(String movementId) { return mongoTemplate.findById(movementId,Movement.class); }
|
1、查询好友动态
- 理解数据库表结构(3张表)
- 时间线表->动态详情表
- Vo对象转化
2、查询推荐动态
- 推荐动态流程
- 基于List的数据分页和MongoDB随机获取数据
3、查询单条动态
2、圈子互动
需求分析:

圈子互动是指用户对动态的点赞、喜欢、评论等操作。
点赞、喜欢、评论等均可理解为用户对动态的互动。
表设计:
数据库表:comment
1 2 3 4 5 6 7 8 9 10 11 12 13
| 将数据记录到表中:保存到MongoDB中 互动表需要几张:需要一张表即可(quanzi_comment) 里面的数据需要分类:通过字段commentType 1-点赞,2-评论,3-喜欢 { "_id" : ObjectId("5fe7f9263c851428107cd4e8"), "publishId" : ObjectId("5fae53947e52992e78a3afa5"), "commentType" : 1, "userId" : NumberLong(1), "publishUserId" : NumberLong(1), "created" : NumberLong(1609038118275), "likeCount" : 0, "_class" : "com.tanhua.domain.mongo.Comment" }
|

问题分析:
如果我们查询 动态数据,每一条动态需要添加冗余字段
优势:查询动态表一次性获取点赞,喜欢,评论数量
缺点: 进行点赞,喜欢,评论时需要维护对应的字段


数据存储位置:redis,mongodb
mongodb中的数据
- 在动态详情Movement表中,加入喜欢,点赞,评论数量:检查数据库访问压力
- 圈子互动的表 comment
- 互动完成(点赞,喜欢):不仅要将数据保存到mongo中,需要记录到redis中
- 页面查询圈子列表时,可以从redis中判断是否有点赞,和喜欢历史
2.1、环境搭建
2.1.1 创建API接口
1 2 3
| public interface CommentApi { }
|
2.1.2 创建API实现类
1 2 3 4 5 6
| @DubboService public class CommentApiImpl implements CommentApi {
@Autowired private MongoTemplate mongoTemplate; }
|
2.1.3 Movement对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "movement") public class Movement implements java.io.Serializable {
private ObjectId id; private Long pid; private Long created; private Long userId; private String textContent; private List<String> medias; private String longitude; private String latitude; private String locationName; private Integer state = 0;
private Integer likeCount = 0; private Integer commentCount = 0; private Integer loveCount = 0; public Integer statisCount(Integer commentType) { if (commentType == CommentType.LIKE.getType()) { return this.likeCount; } else if (commentType == CommentType.COMMENT.getType()) { return this.commentCount; } else { return loveCount; } } }
|
2.1.4 实体类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.tanhua.domain.mongo;
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document;
@Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "comment") public class Comment implements java.io.Serializable{ private ObjectId id; private ObjectId publishId; private Integer commentType; private String content; private Long userId; private Long publishUserId; private Long created; private Integer likeCount = 0; }
|
2.1.5 VO对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Data @NoArgsConstructor @AllArgsConstructor public class CommentVo implements Serializable {
private String id; private String avatar; private String nickname;
private String content; private String createDate; private Integer likeCount; private Integer hasLiked;
public static CommentVo init(UserInfo userInfo, Comment item) { CommentVo vo = new CommentVo(); BeanUtils.copyProperties(userInfo, vo); BeanUtils.copyProperties(item, vo); vo.setHasLiked(0); Date date = new Date(item.getCreated()); vo.setCreateDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)); vo.setId(item.getId().toHexString()); return vo; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public enum CommentType {
LIKE(1), COMMENT(2), LOVE(3);
int type;
CommentType(int type) { this.type = type; }
public int getType() { return type; } }
|
2.2、动态评论

功能包括:查询评论列表,发布评论,对评论点赞和取消点赞。

2.2.1 分页列表查询
1 2 3 4 5 6 7 8
| @GetMapping public ResponseEntity findComments(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize, String movementId) { PageResult pr = commentsService.findComments(movementId,page,pagesize); return ResponseEntity.ok(pr); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public PageResult findComments(String movementId, Integer page, Integer pagesize) { List<Comment> list = commentApi.findComments(movementId,CommentType.COMMENT,page,pagesize); if(CollUtil.isEmpty(list)) { return new PageResult(); } List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class); Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null); List<CommentVo> vos = new ArrayList<>(); for (Comment comment : list) { UserInfo userInfo = map.get(comment.getUserId()); if(userInfo != null) { CommentVo vo = CommentVo.init(userInfo, comment); vos.add(vo); } } return new PageResult(page,pagesize,0l,vos); }
|
1 2 3 4 5 6 7 8 9 10 11
| public List<Comment> findComments(String movementId, CommentType commentType, Integer page, Integer pagesize) { Query query = Query.query(Criteria.where("publishId").is(new ObjectId(movementId)).and("commentType") .is(commentType.getType())) .skip((page -1) * pagesize) .limit(pagesize) .with(Sort.by(Sort.Order.desc("created"))); return mongoTemplate.find(query,Comment.class); }
|
2.2.2 发布评论

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RestController @RequestMapping("/comments") public class CommentsController {
@Autowired private CommentsService commentsService;
@PostMapping public ResponseEntity saveComments(@RequestBody Map map) { String movementId = (String )map.get("movementId"); String comment = (String)map.get("comment"); commentsService.saveComments(movementId,comment); return ResponseEntity.ok(null); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Service @Slf4j public class CommentsService {
@DubboReference private CommentApi commentApi;
public void saveComments(String movementId, String comment) { Long userId = UserHolder.getUserId(); Comment comment1 = new Comment(); comment1.setPublishId(new ObjectId(movementId)); comment1.setCommentType(CommentType.COMMENT.getType()); comment1.setContent(comment); comment1.setUserId(userId); comment1.setCreated(System.currentTimeMillis()); Integer commentCount = commentApi.save(comment1); log.info("commentCount = " + commentCount); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public Integer save(Comment comment) { Movement movement = mongoTemplate.findById(comment.getPublishId(), Movement.class); if(movement != null) { comment.setPublishUserId(movement.getUserId()); } mongoTemplate.save(comment); Query query = Query.query(Criteria.where("id").is(comment.getPublishId())); Update update = new Update(); if(comment.getCommentType() == CommentType.LIKE.getType()) { update.inc("likeCount",1); }else if (comment.getCommentType() == CommentType.COMMENT.getType()){ update.inc("commentCount",1); }else { update.inc("loveCount",1); } FindAndModifyOptions options = new FindAndModifyOptions(); options.returnNew(true) ; Movement modify = mongoTemplate.findAndModify(query, update, options, Movement.class); return modify.statisCount(comment.getCommentType() ); }
|
测试API代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @RunWith(SpringRunner.class) @SpringBootTest(classes = AppServerApplication.class) public class CommentApiTest {
@DubboReference private CommentApi commentApi;
@Test public void testSave() { Comment comment = new Comment(); comment.setCommentType(CommentType.COMMENT.getType()); comment.setUserId(106l); comment.setCreated(System.currentTimeMillis()); comment.setContent("测试评论"); comment.setPublishId(); commentApi.save(comment); } }
|

2.3、点赞
Reids缓存,常使用基础String存储。对于点赞等需求是否合适呢?
如果使用String类型进行存储,那么存储格式如图所示,存在如果动态点赞数据量大的情况。
Key较多,不易维护。

使用Hash 进行存储:
使用动态id进行作为key,字段作为hashKey

接口:

点赞思路:
MongoDB操作:
Redis数据操作:

2.3.1、编写Controller
修改MovementsController
代码,添加点赞与取消点赞方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@GetMapping("/{id}/like") public ResponseEntity like(@PathVariable("id") String movementId) { Integer likeCount = commentsService.likeComment(movementId); return ResponseEntity.ok(likeCount); }
@GetMapping("/{id}/dislike") public ResponseEntity dislike(@PathVariable("id") String movementId) { Integer likeCount = commentsService.dislikeComment(movementId); return ResponseEntity.ok(likeCount); }
|
2.3.2、编写Service
创建CommentService
,添加点赞与取消点赞方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public Integer likeComment(String movementId) { Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); if(hasComment) { throw new BusinessException(ErrorResult.likeError()); } Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LIKE.getType()); comment.setUserId(UserHolder.getUserId()); comment.setCreated(System.currentTimeMillis()); Integer count = commentApi.save(comment); String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().put(key,hashKey,"1"); return count; }
public Integer dislikeComment(String movementId) { Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); if(!hasComment) { throw new BusinessException(ErrorResult.disLikeError()); } Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LIKE.getType()); comment.setUserId(UserHolder.getUserId()); Integer count = commentApi.delete(comment); String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().delete(key,hashKey); return count; }
|
2.3.3、修改API服务
1 2 3 4 5 6 7 8
| public Boolean hasComment(String movementId, Long userId, CommentType commentType) { Criteria criteria = Criteria.where("userId").is(userId) .and("publishId").is(new ObjectId(movementId)) .and("commentType").is(commentType.getType()); Query query = Query.query(criteria); return mongoTemplate.exists(query,Comment.class); }
|
删除互动数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public Integer delete(Comment comment) { Criteria criteria = Criteria.where("userId").is(comment.getUserId()) .and("publishId").is(comment.getPublishId()) .and("commentType").is(comment.getCommentType()); Query query = Query.query(criteria); mongoTemplate.remove(query,Comment.class); Query movementQuery = Query.query(Criteria.where("id").is(comment.getPublishId())); Update update = new Update(); if(comment.getCommentType() == CommentType.LIKE.getType()) { update.inc("likeCount",-1); }else if (comment.getCommentType() == CommentType.COMMENT.getType()){ update.inc("commentCount",-1); }else { update.inc("loveCount",-1); } FindAndModifyOptions options = new FindAndModifyOptions(); options.returnNew(true) ; Movement modify = mongoTemplate.findAndModify(movementQuery, update, options, Movement.class); return modify.statisCount(comment.getCommentType() ); }
|
2.3.4、修改查询动态点赞数
修改之前的查询圈子列表代码,从redis查询是否具有操作记录
2.4、喜欢
喜欢和取消喜欢:和刚才的点赞与取消点赞基本上市一模一样的!操作的类型comment_type=3,操作的字段loveCount
MovementsController
修改MovementsController
代码,添加喜欢与取消喜欢方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@GetMapping("/{id}/love") public ResponseEntity love(@PathVariable("id") String movementId) { Integer likeCount = commentsService.loveComment(movementId); return ResponseEntity.ok(likeCount); }
@GetMapping("/{id}/unlove") public ResponseEntity unlove(@PathVariable("id") String movementId) { Integer likeCount = commentsService.disloveComment(movementId); return ResponseEntity.ok(likeCount); }
|
修改CommentService
,添加点赞与取消点赞方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public Integer loveComment(String movementId) { Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LOVE); if(hasComment) { throw new BusinessException(ErrorResult.loveError()); } Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LOVE.getType()); comment.setUserId(UserHolder.getUserId()); comment.setCreated(System.currentTimeMillis()); Integer count = commentApi.save(comment); String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().put(key,hashKey,"1"); return count; }
public Integer disloveComment(String movementId) { Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LOVE); if(!hasComment) { throw new BusinessException(ErrorResult.disloveError()); } Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LOVE.getType()); comment.setUserId(UserHolder.getUserId()); Integer count = commentApi.delete(comment); String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().delete(key,hashKey); return count; }
|