本项目是一个校内互动交流平台,主要涉及模块有权限模块、帖子模块、性能模块、通知模块、搜索模块。主要使用的技术有SpringBoot,SpringMVC,MyBatis,MySQL,Redis,Kafka。

权限模块主要实现了注册登录和权限管理等功能。
帖子模块主要实现了发帖,评论和私信等功能。
性能模块使用Redis实现点赞,关注和网站数据统计等功能。
通知模块使用Kafka实现系统通知功能。
搜索模块使用Elasticsearch实现帖子搜索功能

1. 权限模块

1.1 注册

输入账户,密码,确认密码,邮箱,点击注册按钮。根据 /register(post)传入User。调用 userService.register 方法。

userService.register 方法首先根据用户名和邮箱查询数据库看是否已经存在,如果存在,返回错误信息。如果不存在,对User加入 Salt 属性,即五位随机字符串,为了和密码拼接后进行md5加密。将加密后的密码存入数据库。分别设置Type,Status为0(Type 0普通用户 1管理员 2版主;Status 0未激活 1已激活)。设置随机字符串激活码。设置用户头像和创建时间。将User插入数据库。

使用JavaMailSender发送激活邮件。将可能带有错误信息的Map返回。

LoginController 层得到 Map 判断是否为空,为空则注册成功,返回 /site/operate-result 页面。否则返回 /site/register 页面,并将错误信息加入model中。

在邮箱中点击激活链接,调用LoginController层的 /activation/{userId}/{code} 。调用userService.activation(userId, code)方法。首先判断 User 中的status是否已经等于1,已经等于1返回 重复激活 信息。然后判断 User 中的激活码是否与code相等,等则设置status为1,返回激活成功。不等返回激活失败。

1.2 登录

首先在首页点击登录或者在其他页面转入登录页面时,验证码路径访问 “/kaptcha” 。

在 LoginController 层中根据 KaptchaConfig 配置类中的 kaptchaProducer (google提供)来生成验证码文本和图像, 生成随机字符串作为Redis中验证码的key,将key放在cookie中通过response传给浏览器。然后利用key将验证码文本存入Redis,设置失效时间为60s。为了判断用户在登录时传入的code是否等于该文本。

再通过response将图像返回给浏览器。

在登录页面输入用户名,密码,验证码和是否勾选“记住我”。将对应的username,password,code,rememberme通过 “/login” 传入 login 方法。

Llogin方法在参数中使用 @CookieValue 注解从cookie中取出Redis中验证码文本的key,判断一下key是否为空,不为空则取出Redis中的验证码文本判断是否和code相等,不等返回Map。然后根据remember选取登录凭证超时时间。然后将username,password 和超时时间 传入给userService.login 方法。

userService.login 方法拿到 三个值先判空,空则返回Map。然后根据username查询数据库看账号是否存在。然后查询status看账户是否已经激活。最后验证密码(调取salt和password拼接后使用md5加密再和数据库中密码比对)。然后生成登录凭证,见LoginTicket,将登陆凭证放入Redis,key为随机字符串Ticket字段。将凭证的key放入Map并返回给 LoginController 层。

LoginController 层拿到Map。查看其中是否有凭证的key,有将凭证的key放入cookie,设置过期时间,返回给浏览器,重定向转入index。无则将Map中报错信息加入model,转入login。

1.3 权限管理

2. 帖子模块

2.1 发帖

使用 Ajax 在 index 页面发送异步请求(局部刷新)。在 Controller 层返回 Json 数据显示。

  1. 前提用户已经登录,否则不显示 “我要发布” 按钮。(通过hostHolder中的 loginUser 是否为空判断)
  2. 在首页点击 “我要发布”,填写标题和正文,映射到DiscussPostController(/discuss)层的 /add 路径。新建 DiscussPost 类,通过 hostHolder 的 User 设置 id,设置 title,content,createTime。其余默认为0。使用 discussPostService.addDiscussPost(post) 方法将其插入数据库。
  3. discussPostService.addDiscussPost 方法将post 中的 title,content 转义HTML标记和过滤敏感词。然后调用 discussPostMapper.insertDiscussPost(post) 存入数据。
  4. 触发发帖事件,构造 Topic 为 TOPIC_PUBLISH 的 Event,需要设置userId,EntityType,EntityId。然后使用 eventProducer.fireEvent 开始生产。(更新Elasticsearch 服务器中的帖子)
  5. 返回发布成功 Json 信息,异步显示,并刷新网页。

附加:当点击某个帖子时,进入详情页面。该页面首先将帖子信息返回显示,然后将该帖子评论查库返回,同时将评论的评论也返回。因为某个用户可以对帖子进行评论,也可以对评论进行评论,反映在评论表中就是entity_type字段不同,1是帖子,2是评论。

2.2 评论

添加评论有三种方式:① 回帖,② 回复评论,③ 回复某人的评论。

  1. ① 在 discuss-detail 页面点击最下方的回帖,映射到 CommentController 层的 /add/{discussPostId}。传入了 entityType = 1 和 entityId = post.id 。
  2. ② 在评论下方回复,与①映射相同,传入entityType = 2 和 entityId = comment.id。
  3. ③ 对某人的评论回复,与①映射相同,传入entityType = 2 和 entityId = comment.id 和 targetId。
  4. 对传入的 comment 进一步设置userId,Status,CreateTime。然后调用commentService.addComment(comment) 方法插入数据库。
  5. commentService.addComment 使用了事务注解。该方法对 comment 的 content 进行转义HTML标记和过滤敏感词。然后使用 commentMapper.insertComment 插入评论。然后通过 commentMapper.selectCountByEntity 查询帖子的评论数量(通过entitId和entityType),再使用 discussPostService.updateCommentCount 方法将评论数量插入帖子详情表。
  6. 触发 Topic 为 TOPIC_COMMENT 的事件: 构建Event,设置当前登录用户id,被评论对象的type,被评论对象的id,被评论对象所在的帖子id(因为被评论的对象可能是帖子或者回复),被评论对象的用户id。然后调用 eventProducer 将 Event 发布到指定 Topic。
  7. 然后判断一下评论的是不是帖子,如果是则触发发帖事件。构造 Topic 为 TOPIC_PUBLISH 的 Event,需要设置userId,EntityType,EntityId。然后使用 eventProducer.fireEvent 开始生产。(更新Elasticsearch 服务器中的帖子)
  8. 重定向 return “redirect:/discuss/detail/” + discussPostId。 刷新帖子详情页面。

2.3 私信

分为两部分:
一、① 在消息页面发私信 ,② 在与某人会话中给TA发私信。
二、将未读消息转为已读。(其实应该放置在 1.6 私信列表 )

  1. 在弹出框中填写发送对象和内容,点击发送。利用 letter.js 发送ajax异步请求。映射到 MessageController 层的 /letter/send 下。构造 message 对象,包括通过发送对象名称查询对象id,拼接 ConversationId。最后将 message 传递给 messageService.addMessage 方法。
  2. messageService.addMessage 方法将message 中的 content 进行转义HTML标记和过滤敏感词。然后插入数据库。MessageController 层中如果发送对象为空,返回 json 错误信息。否则返回 code=0。
  3. letter.js 接受到返回数据,显示信息并重新加载当前页面。②与① 功能实现都是调用的 letter.js。只不过在 ② 中弹出框中自动填入了发送对象名称。
  4. 接受私信的用户在打开私信详情页面时,未读消息就应该变成已读。在 MessageController 层的 /letter/detail/{conversationId} 路径下实现该功能。将私信列表传给 getLetterIds 函数,获得 id 列表。使用 messageService.readMessage 方法将 id 列表代表的 message 的 status 改为1。

3. 性能模块

3.1 点赞

前提:在 util 的 RedisKeyUtil 工具类中写 getEntityLikeKey 方法,传入 entityType, entityId 返回某个实体(帖子或回复)的赞的key值 。
分为两种:① 给帖子点赞 ② 给回复点赞。

  1. ① 在帖子详情页面对帖子内容点赞,按钮通过 discuss.js 将 entityType=0 , 帖子id:entityId 和 entityUserId 通过 /like 映射传递给 LikeController 层中的 like 方法。
  2. like 方法通过调用 likeService 层的 like 进行点赞功能,然后再依次调用 findEntityLikeCount,findEntityLikeStatus 方法查询该帖子的点赞数量,查询当前登录用户的点赞状态。
  3. likeService 层的 like 方法首先判断是否已经点过赞了,如果没有,就在Redis中该帖子key对应的set中加入点赞者的userId,并且对该帖子用户的赞的总数加一。如果已经赞过则移除UserId,并且对该帖子用户的赞的总数减一。(此处使用了事务,因为需要同时进行两个业务)
  4. 触发 Topic 为 TOPIC_LIKE 的事件: 构建Event,设置当前登录用户id,被点赞对象的type,被点赞对象的id,被点赞对象所在的帖子id(因为被点赞的对象可能是帖子或者回复),被点赞对象的用户id。然后调用 eventProducer 将 Event 发布到指定 Topic。
  5. 将点赞的数量和状态装入map,通过json字符串返回。在浏览器中显示。
  6. ② 在帖子详情页面对回复点赞,按钮通过 discuss.js 将 entityType=1回复id:entityId 和 entityUserId 通过 /like 映射传递给 LikeController 层中的 like 方法。

: 另外在 HomeController 层中 /index 映射的方法中,增加向浏览器返回帖子赞的数量的功能。在 DiscussPostController 层中 /detail/{discussPostId} 映射的方法中,增加向浏览器返回帖子和回复列表的赞数量和状态的功能。

3.2 关注

前提:在 util 的 RedisKeyUtil 工具类中写 getFolloweeKey 和 getFollowerKey 方法,返回某个用户关注的实体的key和某个实体拥有的粉丝的key。

  1. 在他人个人主页点击关注,根据 profile.js 异步请求映射到 FollowController 层的 /follow。传入entityType 和 entityId。然后调用 followService.follow 方法进行关注。
  2. followService.follow 方法首先根据 userId, entityType, entityId 获取到前提中的两个key,开启事务,对当前登录用户的关注的key对应的 ZSet 加入 entityId。被关注用户的粉丝的key对应的 ZSet 加入 userId(Redis存的是有序Set,score采用当前时间插入)。
  3. 触发 Topic 为 TOPIC_FOLLOW 的事件: 构建Event,设置当前登录用户id,被关注对象的type,被关注对象的id,被关注对象的用户id(与被关注对象的id相同)。然后调用 eventProducer 将 Event 发布到指定 Topic。
  4. 返回关注成功Json。
  5. 此时按钮显示已关注,若再次点击则映射到 /unfollow。调用 followService.unfollow 方法取消关注。与第2步骤逻辑基本相同,分别减一。

3.3 网站数据统计

使用的Redis数据结构:
在这里插入图片描述
UV统计用户数量(包括不登录用户),DAU统计登录用户数量。
在这里插入图片描述
首先UV和DAU通过Redis存储。根据今日日期定义单日UV和单日DAU的key,类似于 uv:20220516。通过起始日期和终止日期定义区间UV和区间DAU的key,类似于 uv:20220516:20220517。

  1. 新增 DataInterceptor 拦截器,发送请求时,通过ip新增单日UV,通过UserId新增单日DAU。
  2. 在网址上直接输入 http://localhost:8080/community/data 映射到 DataController 层的 /data。返回统计页面。
  3. 在网站UV上选好日期,点击开始统计。映射到 /data/uv 路径。通过 dataService.calculateUV 方法查询UV数量。
  4. calculateUV 方法将区间中每个日期UV的key放入List,然后使用 opsForHyperLogLog().union 将所有日期的值进行合并,将值赋给区间UV的key。返回区间UV的数量。DataController 层将数量返回给浏览器。
  5. 在活跃用户上,点击开始统计,映射到 /data/dau 路径。通过 dataService.calculateDAU 方法查询DAU数量。
  6. calculateDAU 方法将区间中每个日期DAU的key放入List。然后对所有日期的值进行or运算,将值赋给区间DAU的key,返回数量。DataController 层将数量返回给浏览器。
  7. 在 SecurityConfig 中配置只有管理员才能访问该路径。

4. 通知模块-系统通知

4.1 发送系统通知

  1. 构建事件 Event 实体类。包含字段:

private String topic; // 存放消息空间
private int userId; // 触发事件用户id
private int entityType; // 事件目标类型
private int entityId; // 事件目标id
private int entityUserId ;// 事件目标所有者id
private Map<String, Object> data = new HashMap<>(); // 其他

  1. 在 event 文件夹下构建 EventProducer 类,调用 KafkaTemplate 将事件发布到指定的主题。
@Component
public class EventProducer {

    @Autowired
    private KafkaTemplate kafkaTemplate;
    
    // 处理事件
    public void fireEvent(Event event) {
        // 将事件发布到指定的主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}
  1. 在 event 文件夹下构建 EventConsumer 类,监听三种类型topic,即:评论,点赞,关注。根据收到的 event 构建 message 对象,使用 messageService 插入数据库。
@Component
public class EventConsumer implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);

    @Autowired
    private MessageService messageService;

    @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);
        if (event == null) {
            logger.error("消息格式错误!");
            return;
        }

        // 发送站内通知
        Message message = new Message();
        // 系统发送 设为 1
        message.setFromId(SYSTEM_USER_ID);
        message.setToId(event.getEntityUserId());
        // ConversationId 设为 topic。
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());
        
        // message 的 content 
        Map<String, Object> content = new HashMap<>();
        content.put("userId", event.getUserId());
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());

        if (!event.getData().isEmpty()) {
            for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }
        
        // 保存为 json 字符串
        message.setContent(JSONObject.toJSONString(content));
        messageService.addMessage(message);
    }
}
  1. 在评论,点赞,关注时加入触发事件代码。在 帖子模块(核心)1.5小节:CommentController,点赞关注模块(引入Redis)1.2小节:LikeController,点赞关注模块(引入Redis)1.4.1小节:FollowController中加入相应代码,即构建event并且调用 eventProducer 发布事件。

4.2 显示系统通知

4.2.1 点击系统通知

  1. 点击消息内的系统通知,映射到 MessageController 中的 /notice/list。会分别显示评论,赞,关注三类通知的一个最新消息。
  2. 以评论为例:会调用 messageService.findLatestNotice 方法查询到最近的评论通知,将 message 中的 content 反html转义后从json转为map对象,然后将各种信息传入返回给浏览器的map,同时在该map中装入评论通知总数和未读评论通知数。然后将map装入model返回给浏览器。
  3. 赞和关注模块同样如上。最后再将所有的未读数量查询出,装入model。

4.2.2 系统通知详情页面

  1. 点击评论,赞,关注三类通知的某一类通知,会映射到 MessageController 层的 /notice/detail/{topic}。设置分页信息。通过 messageService.findNotices 方法查询通知列表。
  2. 将列表中每个message装入map,并且将每个message中的content反转义并且从json转换为map,将该map中的信息装入返回给浏览器的map。将每个map装入list,将list装入model。
  3. 使用 messageService.readMessage 方法将列表中的 message 设置为已读。
  4. 返回 /site/notice-detail。

最后使用 MessageInterceptor 拦截器查询出私信未读数量和系统通知未读数量,将其加和放入modelAndView,在顶部栏的消息上显示总的未读数量。

5. 搜索模块-帖子搜索

5.1 Spring 整合 Elasticsearch

  1. 在pom.xml 文件中添加 spring-boot-starter-data-elasticsearch 依赖。
  2. 在 application.properties 中设置配置。
  3. Elasticsearch 和 Redis 底层均依赖于 netty,存在冲突。在CommunityApplication 做系统配置修改解决冲突。
  4. 对 DiscussPost 实体类加注解,使 Elasticsearch 能够识别。在 Dao 层加入 DiscussPostRepository 接口继承于 ElasticsearchRepository ,即可直接调用增删改查方法。
  5. 在 ElasticsearchTests 测试类将帖子插入到 Elasticsearch 服务器中并进行简单查询。

5.2 开发社区搜索功能

5.2.1 搜索功能

  1. 在搜索框搜索关键字,映射到 SearchController 层的 /search。调用 elasticsearchService.searchDiscussPost 方法,传入关键词和分页信息。
  2. searchDiscussPost 方法构造一个 searchQuery 来实现标题和内容的多匹配查询,同时根据创建时间等条件设置倒序。然后调用 elasticTemplate.queryForPage 的方法来进行查询,并且将匹配到的字段进行标红。然后将命中的帖子列表返回。
  3. 然后根据帖子列表聚合数据,将每个帖子的用户和点赞数量也放入其中。设置分页信息。
  4. 返回 /site/search。

5.2.2 更新Elasticsearch 服务器中的帖子

发布帖子,增加评论时要将帖子异步提交到 Elasticsearch 服务器。利用kafka实现。

  1. 在 DiscussPostController 层的 /add 映射下,触发发帖事件,构造 Topic 为 TOPIC_PUBLISH 的 Event,需要设置userId,EntityType,EntityId。然后使用 eventProducer.fireEvent 开始生产。见 帖子模块(核心)1.2小节。
  2. 在 CommentController 层的 /add/{discussPostId} 映射下,判断一下评论的是不是帖子,如果是则触发发帖事件。同上。见 帖子模块(核心)1.5小节。
  3. 在 EventConsumer 中构建消费发帖事件,监听的 Topic 为 TOPIC_PUBLISH。判断消息是否为空,不为空将消息转为 Event,然后根据 Event 的 EntityId 获取到帖子,然后调用 discussRepository.save 方法将帖子插入到Elasticsearch 服务器中。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐