课程说明
- 圈子动态查询
- 圈子实现评论
- 圈子实现点赞、喜欢功能
- 圈子实现评论
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
/** * 查询好友动态 */ @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
//好友动态 public PageResult findFriendMovements(Integer page, Integer pagesize) { //1、获取当前用户id Long userId = UserHolder.getUserId(); //2、查询数据列表 List<Movement> items = movementApi.findFriendMovements(userId,page,pagesize); //3、非空判断 if(CollUtil.isEmpty(items)) { return new PageResult(); } //4、获取好友用户id List<Long> userIds = CollUtil.getFieldValues(items, "userId", Long.class); //5、循环数据列表 Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : items) { //5、一个Movement构建一个Vo对象 UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } //6、构建返回值 return new PageResult(page,pagesize,0L,vos); }
movementApi
@Override public List<Movement> findFriendMovements(Long friendId, Integer page, Integer pagesize) { //1、查询好友时间线表 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); //2、提取动态id集合 List<ObjectId> movementIds = CollUtil.getFieldValues(lines, "movementId", ObjectId.class); //3、根据动态id查询动态详情 Query movementQuery = Query.query(Criteria.where("id").in(movementIds)); return mongoTemplate.find(movementQuery, Movement.class); }
1.2、查询推荐动态
推荐动态是通过推荐系统计算出的结果,现在我们只需要实现查询即可,推荐系统在后面的课程中完成。
推荐系统计算完成后,会将结果数据写入到Redis中,数据如下:
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层编写方法
- 随机获取
- 根据pid列表查询
1.2.3、代码实现
Constants
package com.tanhua.commons.utils; //常量定义 public class Constants { //手机APP短信验证码CHECK_CODE_ public static final String SMS_CODE = "CHECK_CODE_"; //推荐动态 public static final String MOVEMENTS_RECOMMEND = "MOVEMENTS_RECOMMEND_"; //推荐视频 public static final String VIDEOS_RECOMMEND = "VIDEOS_RECOMMEND_"; //圈子互动KEY public static final String MOVEMENTS_INTERACT_KEY = "MOVEMENTS_INTERACT_"; //动态点赞用户HashKey public static final String MOVEMENT_LIKE_HASHKEY = "MOVEMENT_LIKE_"; //动态喜欢用户HashKey public static final String MOVEMENT_LOVE_HASHKEY = "MOVEMENT_LOVE_"; //视频点赞用户HashKey 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"; //jwt加密盐 public static final String JWT_SECRET = "itcast"; //jwt超时时间 public static final int JWT_TIME_OUT = 3_600; }
MovementController
/** * 推荐动态 */ @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
//推荐动态 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); //5、循环数据列表 Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : list) { //5、一个Movement构建一个Vo对象 UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } //6、构建返回值 return new PageResult(page,pagesize,0L,vos); }
movementApi
//随机获取 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(); } //根据PID查询 public List<Movement> findByPids(List<Long> pids) { Query query = Query.query(Criteria.where("pId").in(pids)); return mongoTemplate.find(query, Movement.class); }
1.3、根据id查询动态
根据id查询动态:当手机端查看评论内容时(需要根据动态id,查询动态详情),后续再去查询评论列表
1.3.1、接口文档
API接口文档:http://192.168.136.160:3000/project/19/interface/api/151
1.3.2、代码实现
MovementController
/** * 根据id查询动态 */ @GetMapping("/{id}") public ResponseEntity findById(@PathVariable("id") String movementId) { MovementsVo vo = movementService.findMovementById(movementId); return ResponseEntity.ok(vo); }
MovementService
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
@Override public Movement findById(String movementId) { return mongoTemplate.findById(movementId,Movement.class); }
2、圈子互动
点赞、喜欢、评论等均可理解为用户对动态的互动。
数据库表:quanzi_comment
将数据记录到表中:保存到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接口
public interface CommentApi { }
2.1.2 创建API实现类
@DubboService public class CommentApiImpl implements CommentApi { @Autowired private MongoTemplate mongoTemplate; }
2.1.3 Movement对象
@Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "movement") public class Movement implements java.io.Serializable { private ObjectId id; //主键id //redis实现,使用Mongodb实现 private Long pid; //Long类型,用于推荐系统的模型(自动增长) private Long created; //发布时间 private Long userId; private String textContent; //文字 private List<String> medias; //媒体数据,图片或小视频 url private String longitude; //经度 private String latitude; //纬度 private String locationName; //位置名称 private Integer state = 0;//状态 0:未审(默认),1:通过,2:驳回 //补充字段 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 实体类对象
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; //发布id private Integer commentType; //评论类型,1-点赞,2-评论,3-喜欢 private String content; //评论内容 private Long userId; //评论人 private Long publishUserId; //被评论人ID private Long created; //发表时间 private Integer likeCount = 0; //当前评论的点赞数 }
2.1.5 VO对象
@Data @NoArgsConstructor @AllArgsConstructor public class CommentVo implements Serializable { private String id; //评论id private String avatar; //头像 private String nickname; //昵称 private String content; //评论 private String createDate; //评论时间 private Integer likeCount; //点赞数 private Integer hasLiked; //是否点赞(1是,0否) 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; } }
2.1.6 CommentType枚举
/** * 评论类型:1-点赞,2-评论,3-喜欢 */ 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 分页列表查询
CommentController
//分页查询评理列表 @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); }
CommentService
//分页查询评理列表 public PageResult findComments(String movementId, Integer page, Integer pagesize) { //1、调用API查询评论列表 List<Comment> list = commentApi.findComments(movementId,CommentType.COMMENT,page,pagesize); //2、判断list集合是否存在 if(CollUtil.isEmpty(list)) { return new PageResult(); } //3、提取所有的用户id,调用UserInfoAPI查询用户详情 List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class); Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null); //4、构造vo对象 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); } } //5、构造返回值 return new PageResult(page,pagesize,0l,vos); }
CommentAPI
//分页查询 public List<Comment> findComments(String movementId, CommentType commentType, Integer page, Integer pagesize) { //1、构造查询条件 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"))); //2、查询并返回 return mongoTemplate.find(query,Comment.class); }
2.2.2 发布评论
CommentController
@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); } }
CommentService
@Service @Slf4j public class CommentsService { @DubboReference private CommentApi commentApi; //发布评论 public void saveComments(String movementId, String comment) { //1、获取操作用户id Long userId = UserHolder.getUserId(); //2、构造Comment Comment comment1 = new Comment(); comment1.setPublishId(new ObjectId(movementId)); comment1.setCommentType(CommentType.COMMENT.getType()); comment1.setContent(comment); comment1.setUserId(userId); comment1.setCreated(System.currentTimeMillis()); //3、调用API保存评论 Integer commentCount = commentApi.save(comment1); log.info("commentCount = " + commentCount); } }
CommentAPI
//发布评论,并获取评论数量 public Integer save(Comment comment) { //1、查询动态 Movement movement = mongoTemplate.findById(comment.getPublishId(), Movement.class); //2、向comment对象设置被评论人属性 if(movement != null) { comment.setPublishUserId(movement.getUserId()); } //3、保存到数据库 mongoTemplate.save(comment); //4、更新动态表中的对应字段 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); //5、获取最新的评论数量,并返回 return modify.statisCount(comment.getCommentType() ); }
测试API代码
@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、点赞
2.3.1、编写Controller
修改MovementsController
代码,添加点赞与取消点赞方法
/** * 点赞 */ @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
,添加点赞与取消点赞方法
//动态点赞 public Integer likeComment(String movementId) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); //2、如果已经点赞,抛出异常 if(hasComment) { throw new BusinessException(ErrorResult.likeError()); } //3、调用API保存数据到Mongodb 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); //4、拼接redis的key,将用户的点赞状态存入redis 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) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); //2、如果未点赞,抛出异常 if(!hasComment) { throw new BusinessException(ErrorResult.disLikeError()); } //3、调用API,删除数据,返回点赞数量 Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LIKE.getType()); comment.setUserId(UserHolder.getUserId()); Integer count = commentApi.delete(comment); //4、拼接redis的key,删除点赞状态 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服务
判断Comment数据是否存在
//判断comment数据是否存在 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); //判断数据是否存在 }
删除互动数据
//删除 public Integer delete(Comment comment) { //1、删除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); //2、修改动态表中的总数量 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); //5、获取最新的评论数量,并返回 return modify.statisCount(comment.getCommentType() ); }
2.3.4、修改查询动态点赞数
修改之前的查询圈子列表代码,从redis查询是否具有操作记录
2.4、喜欢
喜欢和取消喜欢:和刚才的点赞与取消点赞基本上市一模一样的!操作的类型comment_type=3,操作的字段loveCount
MovementsController
修改MovementsController
代码,添加喜欢与取消喜欢方法
/** * 喜欢 */ @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
修改CommentService
,添加点赞与取消点赞方法
/** * 喜欢 */ @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); }