总体介绍
项目功能分为注册登录、然后用户登录之后可以发布帖子,查看帖子,点赞,回帖,对回帖进行评论和回复,还能通过点击帖子发布者头像关注发布者
注册功能
注册需要账号、密码、确认密码、邮箱,首先service会判断输入是否为空,如果不为空进入下一步,分别通过账号、邮箱查找数据,如果数据不为空提示对应已注册信息。如果前面条件都不满足,进入下一步,创建一个user对象封装好对应信息,同时设置一个随机头像,将数据插入数据库,此时用户还不能使用,需要通过邮件激活,user对象的状态才会发生改变,插入数据之后,就会给邮箱发送邮件
![](https://i-blog.csdnimg.cn/blog_migrate/0951ca98958d4be6cbfb41c59081da4a.png)
链接有激活地址,这个地址拼装的是user的Id和一个激活码,点击链接会访问url映射的方法,并将这两个数据作为参数传入到方法中,发邮件之前activation_code已经封装在user对象传入数据库中,在激活方法中会有三种情况激活成功、已激活不用重复激活、激活码不正确请重新激活,激活成功后账户就能够使用此时user.status为1。需要注意的是密码在传入后端的时候会先拼接一个salt字段,salt字段是随机的5个字符构成的字符串,拼接之后才会进行md5加密存入到数据库中,同时也将salt字符串传入数据库中
登录
这部分功能除了判空以外,登录上的有四种情况,账号不正确,密码不正确,账号未激活,验证码不正确。登录成功之后会创建登录凭证对象,登录凭证有userid,凭证码,status(状态是否有效),过期时间,登录凭证主要用来记录用户的登录操作,留存一个记录,然后将登录凭证存入cookie中,每次请求拦截器都会从cookie中获取ticket,如果ticket不为空,状态有效,当前时间小于过期时间,就会通过ticket中的userid信息获取user对象,存入threadlocal本地线程中,目的就是在本次请求中持有用户,访问完地址映射的方法之后,然后在posthandle方法中,获取本地线程存放的user对象,放入modelandview中返回给客户端,在模板渲染完之后清除threadlocal中的user对象
目前的功能是,用户登录之后可以发布帖子,查看帖子,点赞,回复帖子,还可以通过点击头像关注帖子的发布者
登录之后的功能
发布帖子,点赞,详情帖子查看,回帖,回复,回复帖子下的回复,通过点击发布者头像关注发布者
帖子详情查看
discusspost
283 | 154 | 我要发布标题 | 我要发布正文 |
comment
id | userid | type | entityid | targetid | ||
238 | 154 | 1 | 283 | 0 | 回帖正文 | |
239 | 154 | 2 | 238 | 0 | 回帖下的回复 | |
240 | 154 | 2 | 238 | 154 | 回帖下的回复的回复 |
以上是帖子详情页面,一个帖子,一个回帖,回帖下的回复,回帖下回复的回复,数据库记录
后端代码
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
// 点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
model.addAttribute("likeCount", likeCount);
// 点赞状态
int likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);
model.addAttribute("likeStatus", likeStatus);
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());
// 评论: 给帖子的评论
// 回复: 给评论的评论
// 评论列表,回帖
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 评论VO列表
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO
Map<String, Object> commentVo = new HashMap<>();
// 评论
commentVo.put("comment", comment);
// 作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("likeCount", likeCount);
// 点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("likeStatus", likeStatus);
// 回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复VO列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {//replyList是回帖下的回复
for (Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());//如果是回复的回复,则targetid是回复的回复记录id,如果没有则只是回帖的回复
replyVo.put("target", target);
// 点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
replyVo.put("likeCount", likeCount);
// 点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());
replyVo.put("likeStatus", likeStatus);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
当前帖子的内容通过discusspost对象储存,再通过discusspost的id找到comment中entity_id为discusspostid的记录封装成commentList,当前只找到一条就是commentid为238的数据,再通过遍历commentList,此时取到list中唯一保存的comment对象就是238,通过238再找到entity_id为238的comment此时找到两条一个commentid为239一个240,两条记录封装成replyList,再遍历replyList,取每一个回复comment对象进行加工,加工成replyvolist,然后存入commentvolist,注意遍历的时候遍历的是commentlist,
从commentlist(调用dao层方法返回的数据集合)遍历出的每一个comment对象会和一个replyvolist,作者,点赞数量,点赞状态,replycount封装成一个commentvo map集合存入commentvolist中
replylist(调用dao层返回的回复数据集合),遍历replylist集合,每一个list集合中存放的是回复comment对象,再将这些回复comment和其作者、回复目标(有则存入target user 对象,无则null,前端会通过这个判断
![](https://i-blog.csdnimg.cn/blog_migrate/fd6109f6b864af6e4cd07b458b88b1aa.png)
<span th:if="${rvo.target==null}">
<b class="text-info" th:text="${rvo.user.username}">寒江雪</b>:
</span>
<span th:if="${rvo.target!=null}">
<i class="text-info" th:text="${rvo.user.username}">Sissi</i> 回复
<b class="text-info" th:text="${rvo.target.username}">寒江雪</b>:
</span>
)、点赞数量、点赞状态封装成一个replyvo map集合存入replyvolist,replyvolist集合最后会放到commentvolist中整体返回给前端
点赞
点赞只能在帖子详情页面进行点赞,可以对帖子点赞,回帖点赞,回复点赞,回复的回复点赞
帖子点赞前端
![](https://i-blog.csdnimg.cn/blog_migrate/98f7a2e3fce3f1a06d6b7f9a5f68a15d.png)
回帖点赞前端
![](https://i-blog.csdnimg.cn/blog_migrate/fd5446e73cc9a676f2a24b5de0862371.png)
回复点赞前端
![](https://i-blog.csdnimg.cn/blog_migrate/96313258a245e15f470199d43e078058.png)
回复的回复
![](https://i-blog.csdnimg.cn/blog_migrate/7177403f256db3d1b3d09c904245d8c6.png)
redis中存放的key有两种类型(红色部分为固定部分)
key like:entity:entitiyType:entitiyId value 存放的是userId
key like:user:userId value 存放的是int数据
每一回点击点赞标签,在后端就会判断帖子key中是否包含当前登录的userid,有则代表此时是需要取消点赞,删掉帖子key中包含的当前登录的userid信息,同时,这个帖子的发布者userid的key中int数据自减一,代表帖子发布者获得的赞数量减一。如果是进行点赞操作,业务逻辑与取消点赞业务逻辑相反。
点赞状态查询
当前用户查看点赞状态,只有两种,一个就是已点赞,一个就是未点赞
当 当前用户进入详情页面,后端也会判断,在redis中通过当前查看的帖子类型、id构造的key中通过ismember方法查看value中否有当前登录的userid,如果有就是已点赞状态,没有就是无点赞
其他点赞信息
//查询某实体点赞的数量
public long findEntityLikeCount(int entityType,int entityId){
String entityLikeKey=RedisKeyUtil.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
// 查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;//isMember返回的是true或false
}
//查询某用户获得的赞
public int findUserLikeCount(int userId){
String userLikeKey=RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count==null?0:count.intValue();//intValue是拆箱操作
}
发送系统通知
评论、点赞、关注/取消关注发布通知。在业务处理的时候是作为事件(在kafka中是当作消息来处理)进行处理,首先要创建事件对象,当触发事件的时候将事件相关的消息拼接进去,最终会把事件中的数据存进数据库中,消息是以json字符串格式进行数据发送,在监听方法中将json数据取出转成event对象,取出相应信息,再通过这些信息通过主题类型找出另外所需的数据,封装成message对象,存入message表中。
// 触发评论事件
Event event = new Event()
.setTopic(TOPIC_COMMENT)
.setUserId(hostHolder.getUser().getId())
.setEntityType(comment.getEntityType())
.setEntityId(comment.getEntityId())
.setData("postId", discussPostId); //存放的是
if (comment.getEntityType() == ENTITY_TYPE_POST) {
DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
Comment target = commentService.findCommentById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}
eventProducer.fireEvent(event);
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);//将json字符串转换成指定的实体类对象
if (event == null) {
logger.error("消息格式错误!");
return;
}
// 发送站内通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());//前端还需要拼成一个字符串用户xx评论了xx这些需要以下三个信息,所以需要存起来
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
//event的data属性是map类型,有的主题会往data中存数据,之后将data中map的数据遍历赋值给content
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {//遍历map的其中一个方式
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
messageService.addMessage(message);
}
关注type是3,对帖子点赞是1,对评论点赞是2
message表结构如下,其中status代表消息是否已读
from_id | to_id | conversation_id | content | status | create_time | |
365 | 1 | 154 | comment | {"entityType":2,"entityId":238,"postId":283,"userId":154} | 0 | 2023-03-14 20:11:00.0 |
364 | 1 | 154 | comment | {"entityType":2,"entityId":238,"postId":283,"userId":154} | 0 | 2023-03-14 20:10:25.0 |
363 | 1 | 154 | comment | {"entityType":1,"entityId":283,"postId":283,"userId":154} | 0 | 2023-03-14 20:09:08.0 |
kafka
![](https://i-blog.csdnimg.cn/blog_migrate/b90934b394541eea66daa9e9cc874701.png)
broker:就是kafka的一个服务器
zookeeper:独立的软件不是kafka的东西,用来管理集群
topic:主题,消息队列一般有两种实现方式,一种是点对点,每个数据只会被一个消费者消费就是点对点,还有一种就是发布订阅的方式,可以有很多消费者订阅这个位置,然后读取消息,一个数据可以被多个消费者同时读到,或者先后读到
partition:分区,对主题进行分区,为了增加并发能力
offset:这消息在分区内存放的索引位置,序列
replica:副本,对数据做备份,对分区备份多份数据,分为leader副本和follower副本
![](https://i-blog.csdnimg.cn/blog_migrate/b5c8c4557d8d3bebccdb5f66314b3523.png)
通过spring提供的一个kafkatemplate类发送消息,生产者是主动调的,消费者是被动调的
kafkaListener:kafka的监听器,可以监听一个或多个主题,将消息封装进consumerrecord对象中