牛客网社区项目复盘

该博客围绕社区项目开发展开,涵盖登录注册模块,包括注册与登录功能开发;社区核心功能,如敏感词过滤、帖子发布与显示等;还涉及 Redis 点赞、关注功能,Kafka 异步消息处理,以及 ElasticSearch 搜索引擎实现搜索服务,介绍了各部分的开发步骤与技术运用。

1.登陆注册模块

开发注册功能

开发页面步骤如下:建立实体类(entity)确定具有哪些属性-》构建数据层(Mapper)建表-》构建服务层(service)把数据层存放的数据经过处理传送到项目-》构建controller;

开发注册功能过程:

1.建立实体类:

#显示页面(page)控制用户看到的页面,页面有多少条帖子,当前在第几页

。属性:当前页码(current),总页数(rows),当前页面能看到的上限(limit),页面路径(path)//有路径的话方便后续复用。

#用户(user)。属性:用户账号(id),用户名(username),密码(password),用户类型(type)普通/版主/游客,用户状态(status)登录/游客,用户邮箱(email),用户头像(headerUrl),注册时间(createTime),加密盐值(salt)

#讨论区(discusspost)。属性:主键(id),用户名(user_id),帖子类型(type)普通/置顶,帖子状态(status)普通/精华/拉黑,帖子标题(title),帖子内容(content),评论数量(commentCount),发表时间(createTime);

2.数据层

#用户数据UserMapper:通过用户账户(id),用户邮件(username),用户邮箱(email)

查询到当前用户;更新用户的头像,状态,密码;插入用户

#讨论区discusspostMapper:

显示页面出现行数,通过页码显示相应的讨论区。更新用户的状态,类型。新建页面

3.服务层

#用户服务UserService

注册:空值处理(账号,密码,邮箱不能为空);验证(账号邮箱是否已经存在,验证方式就是查看数据库中该账号是否为空);注册用户(设置所有属性值)

#讨论区服务discusspostService

查询指定用户的讨论帖子并返回指定用户的讨论帖子行数。

4.视图层

把静态资源 css、html、img、js 放到 static 目录下。

把模板 mail、site、index.html 放到 template 目录下。

创建 HomeController,getIndexPage 方法,用 map 集合把帖子和用户封装到一起。

修改 index.html,使用 <th:text="${map.xxx.xxx}" 动态替换。

开发登录功能过程

1.生成验证码

这里用到了一个插件,插件的使用流程为:添加依赖-》创建config包并在里面创建相关类配置属性-》在相应地方使用

添加依赖:kaptcha(2.3.2)

配置属性:在config创建KaptchaConfig类,设置验证码的大小、范围、长度。

使用:在LoginController类里面创建getKaptcha方法,方法实现:生成验证码,将验证码存入session再将验证码输出给浏览器。

2.登录登出

创建实体:登录凭证。登录凭证属性:主键(id),用户名(userId),凭证(ticket),状态(status),到期时间(expired)

Mapper:一般都是增查改。增加登陆凭证(设置好实体中的所有变量),查询,更新所有变量。

Service:在UserService中添加login方法,空值处理(账号密码不能为空),验证(验证数据库中是否有匹配的账户和密码),生成凭证(初始化变量并放入Map中)

3.发送邮件

添加依赖:spring-boot-starter-mail(2.1.5.RELEASE)

配置属性:在配置文件配置主机号,用户,密码,邀请码

# MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=nowcoder@sina.com
spring.mail.password=nowcoder123
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true

使用:创建 MailClient 类,调用 JavaMailSender 发送邮件。使用 thymeleaf 发送 HTML 邮件,调用 TemplateEngine 把信息封装到 HTML 模板。

4.密码加密

为了保证安全,密码不能明文的在网络中进行传输,也不能以明文的形式存到数据库中。
存在数据库的密码 = MD5( 密码 + salt ) 防止密码泄露

社区核心

1.敏感词过滤

敏感词过滤

利用字典树数据结构解决。
三个指针,一个指针首先指向前缀树根节点;另外两个指针指向待过滤信息的头部。交替移动对比,不匹配时候放入结果集,匹配的时候替换为指定符号。

创建 SensitiveFilter 类

    创建静态内部类 TrieNode ,通过 boolean 类型的结束符判断是否匹配到关键字尾部。
    利用 @PostConstruct 注解,在构造方法执行后初始化字典树。
    添加 filter 方法,利用双指针进行匹配,过滤敏感词。

【问题】判断子节点空时,直接添加了一个 new 的子节点,没有将对象赋值给子节点变量。

2.发布帖子

引入 fastjson 依赖,在 CommunityUtil 中新增 getJSONString 方法封装 JSON 信息。

在 DisscussPostMapper 接口新增 insertDiscussPost 方法,并在 disscusspost-mapper.xml 配置 insert 语句。

在 DiscussPostService 新增 addDiscussPost 方法调用 DisscussPostMapper 的 insertDiscussPost 方法,其中需要进行对标题内容和发帖内容进行 HTML 转义以及过滤敏感词。

创建 DiscussPostController 类,新增 addDiscussPost 方法,调用 DiscussPostService 的 addDiscussPost 方法发帖。

在 index.html 中为发帖按钮绑定函数,利用 Ajax 向 DiscussPostController 的 addDiscussPost 方法发送 POST 请求。

3.显示帖子

创建实体类:已经创建discusspost

数据层:创建selectDiscussPostById(),通过具体用户id查找帖子,返回类型为discusspost

服务层:创建findDiscussPostById(int id)调用数据层,得到数据

视图层:

4.显示评论

创建实体类:创建实体类comment。comment属性:主键(id),评论者的id(userId),被评论者的id(targetId),帖子的id(entityId),帖子的类型(status),帖子的创建时间(createTime);

数据层:selectCommentByEntity 方法,根据实体查询一页的评论数据,selectCountByEntity 方法,根据实体查询评论的数量。

服务层:创建 findCommentByEntity方法,调用 CommentMapper 的 selectCommentByEntity 方法。findCountByEntity 方法,调用 CommentMapper 的 selectCountByEntity 方法。

视图层:

5.显示私信列表

创建实体:创建Messsage,属性:主键(Id),发送方(fromId),接收方(to Id),消息所属对话(conversationId),消息内容(content),消息状态(status),创建时间(creatTime)

数据层:创建 MessageMapper 接口,增改查,查出现得最多。

服务层:创建 MessageService,查询会话列表、会话数量、私信列表、私信数量、未读私信数量的方法,更新私信状态,增加私信(即初始化实体中的所有变量)

视图层:创建 MessgaeController,新增 getLetterList 方法,将会话列表信息存储到 Model 对象,返回letter视图。新增getLetterDetail方法,将每个会话具体的私信信息存储到 Model 对象,返回letter-detail视图。

6.添加评论

创建实体:comment类

数据层:在 CommentMapper 接口新增insertComment方法,增加评论。在DiscussPostMapper中也要增加updateCommentCount方法更新评论数量(找到对应id更新为输入的数量)

服务层:在 CommentService 类新增 addComment 方法,调用 CommentMapper 的 insertComment 新增评论,并调用 DiscussPostService 的 updateCommentCount 更新评论数量,使用 @Transactional 注解保证事务。(为什么要使用@Transactional?选事务将只读取在开始读取之前已提交的数据。这有助于防止脏读和非重复读。

视图层:创建 CommentController 类,新增addComment方法,从 hostHolder 获取用户信息,然后调用 CommentService 的addComment方法添加评论。

出现问题:sql 的 xml 文件中绑定参数时,应传入实体类属性名,拼错成数据库字段名(entityId 写成 entity_id)。

7.发送私信

创建实体:Message类

数据层:新增insertMessage方法插入私信记录(初始化所有变量)。新增updateStatus方法修改私信状态,用foreach进行遍历,使用open、separator和close属性来生成SQL语句中的括号和逗号分隔符。

服务层:新增addMessage方法(用setContent对评论内容进行转义,再进行敏感词过滤再将评论插入数据库)。新增readMessage方法读取信息,调用MessageMapper 的updateMessage更新私信的状态为 1。

视图层:新增 getLetterIds 方法,将私信集合中未读私信的 id 添加到 List 集合并返回,在 getLetterDetail 方法调用该方法设置已读。新增 sendLetter 发送私信方法,设置私信信息后调用 MessageService 的 addMessage 发送。

8.统一异常处理:

在 HomeController 中增加getErrorPage方法,返回错误页面。创建 ExceptionAdvice 类

思路:

  • 使用@ControllerAdvice注解定义一个全局异常处理类,该类将处理所有控制器抛出的异常。
  • 定义一个Logger对象,用于记录日志。
  • 使用@ExceptionHandler注解定义一个处理方法,该方法用于处理所有类型为Exception的异常。在这个方法中,首先记录异常信息到日志中,然后遍历异常堆栈信息并记录到日志中
  • 获取请求头中的x-requested-with字段值,用于判断客户端发送的是Ajax请求还是普通请求。
  • 如果x-requested-with字段值为XMLHttpRequest,表示客户端发送的是Ajax请求,则设置响应内容类型为纯文本,并设置字符编码为utf-8。接着获取响应的PrintWriter对象,用于向客户端输出响应内容。最后输出JSON格式的错误信息。
  • 如果x-requested-with字段值不是XMLHttpRequest,表示客户端发送的是普通请求,则直接重定向到错误页面。

9.统一日志处理:

 引入 aspectj 的依赖。

创建 ServiceLogAspect 类,添加@Aspect切面注解,配置切入点表达式,拦截所有 service 包下的方法,利用@Before记录日志。

Redis

redis步骤如下:生成相应的key-》用redistemple书写service层-》书写controller层

配置redis

# RedisProperties
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379

1.点赞

生成相应的key:创建 RedisKeyUtil 工具类, 定义分隔符 : 以及实体获得赞的 key 前缀常量 like:entity。新增 getEntityLikeKey(int entityType,int entityId) 方法,通过实体类型和实体 id 生成对应实体获得赞的 key。

Service层:创建业务层的 LikeService 类注入 RedisTemplate 实例。新增 like 点赞方法,首先通过 RedisKeyUtil 工具类的 getEntityLikeKey 方法获得实体点赞的 key,然后通过 RedisTemplate 对象对 set 集合的 isMember 方法查询 userId 是否存在于对应 key 的 set 集合中,如果存在则移除出点赞的用户集合,如果不存在则添加到点赞的用户集合。新增 findEntityLikeCount 方法查询实体的点赞数量,通过调用 set 集合的 size 方法查询元素个数。新增 findEntityLikeStatus 方法查询某用户对某实体的点赞状态,逻辑如 like 方法,通过 set 集合的 isMember 方法实现。

创建表现层的 LikeController 类:注入 LikeService 和 HostHolder 实例。新增 like 点赞方法,调用业务层的 like 方法进行点赞、调用 findEntityLikeCount 和 findEntityLikeStatus 查询点赞数量和点赞状态,封装到 map 集合,然后通过工具类封装成 JSON 数据返回。(更新首页帖子点赞数量)在表现层的 HomeController 类注入 LikeService 实例。在 getIndexPage 方法在通过 LikeService 类的方法获得点赞数量,存储到 map 集合。

2.收到的赞

对点赞功能进行重构

生成相应的key:在 RedisUnitl 工具类 新增用户获得赞 key 的前缀常量 like:user新增 getUserLikeKey(int userId) 方法,通过用户 id 生成对应用户获得赞的 key。

Service层:LikeService 中:重构 like 方法,在参数列表中加入 entityUserId 表示被点赞用户的 id,用来更新用户的被点赞数量。通过 RedisTemplate 对象的 execute 方法实现事务,保证被点赞用户点和点赞用户的数据更新一致。通过 isMember 方法查询用户的点赞状态,之后通过 mutli 方法开启事务。当用户已点赞时,调用 remove 方法将当前用户从点赞用户的集合中移除,调用 decrement 方法将被点赞用户的被点赞数减 1;当用户未点赞时,调用 add 方法将当前用户添加到点赞用户的集合,调用 increment 方法将被点赞用户的被点赞数加 1。增加 findUserLikeCount 方法,以用户 id 作为 key,调用 get 方法查询用户所获得的点赞数。

Controller层:在 LikeController 中给 like 方法增加 entityUserId 参数即可。

3.关注

增加相关的Key:在RedisKeyUtil工具类里面新增用户关注实体(帖子、评论、用户等)和粉丝(用户)的前缀常量 followee 和 follower。增加getFolloweeKey(int userId, int entityType) 方法(通过用户 id 和实体类型生成用户关注实体的 key)getFollowerKey(int entityType, int entityId) 方法(通过实体类型和实体 id 生成实体用户粉丝的 key)

Service层:运用redis中的事务,调用operations.opsForZSet()中的add()和remove()方法将关注对象移入或移出队列来实现关注或取消关注。

Controller层:新增 findFolloweeCount 方法(调用 zset 的 zcard 方法查询某用户关注的实体数量); findFollowerCount 方法(调用 zset 的 zcard 方法查询某实体的粉丝数量);新增 hasFollowed 方法(根据 zset 的 zscore 方法返回值查询当前用户是否关注某实体)

在 UserController 中新增 getProfilePage 方法获取个人主页。调用 LikeService 的 findUserLikeCount 查询用户获赞数,并添加到 Model 中。调用 FollowService 的findFolloweeCount、findFollowerCount 、hasFollowed 方法分别查询关注数量、粉丝数量、用户是否关注三项信息并添加到 Model 对象中存储。

4.关注列表和粉丝列表

Service层:在FollowService 类,新增 findFollowees 方法(查询用户关注列表,主要通过 zset 的 reverseRange 获取 value 即关注用户的 userId,再查询出其 user,之后通过 score 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回)findFollowers 方法(查询用户粉丝列表,主要通过 zset 的 reverseRange 获取 value 即粉丝的 userId,再查询出其 user,之后通过 score 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回。)

Controller层:新增getFollowees方法,获取关注列表,存入 Model 对象。getFollowers 方法,获取粉丝列表,存入 Model 对象。

Kafka

在项目中,会有一些不需要实时执行但是是非常频繁的操作或者任务,为了提升网站的性能,可以使用异步消息的形式进行发送,再次消息队列服务器kafka来实现。

请大家思考,在本项目中有什么是不需要系统实时响应的?当有其他用户给我们点赞后我们会收到我们自己的帖子被点赞的消息,这个是不需要实时响应的。

所以用到Kafka的地方有:评论后发布通知,点赞后发布通知,关注后发布通知。

Kafka逻辑如下:封装事件对象-》构建生产者-》构建消费者

配置kafka

# KafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=community-consumer-group
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-commit-interval=3000

封装事件对象:构建实体类:event。属性:发送主题(topic),用户名(userId),消息状态(entityType),消息序号(entityId),消息发送方名字(entityUserId),存储数据(Map<String, Object> )

构建生产者:在event包下创建EventProducer,用kafkaTemple中的send()发送实体类中Event数据到消费者。

构建消费者:这里要用到Kafka中的MessageService类发送通知到相应的id即可

ElasticSearch搜索引擎

本项目中的搜索服务依靠ES实现

(1)创建一个新的业务层ElasticsearchService,实现存、删除、搜索帖子等方法。

(2)搜索方法,返回的是一个帖子的页面,参数:关键字、当前第几页、每页显示多少条数据

过程为:首先构造一个查询对象searchQuery,加入参数包括查询位置(title,content),查询顺序(type,score,createtime),分页信息,高亮设置。接着获取命中数据,创建一个集合,在集合中构建一个实体,根据命中的数据去构造这个实体,最终返回。

public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit)

(3)表现层用异步的方法处理帖子。在发帖和增加评论是触发事件,提交到消息队列,再在消费组件中增加消费事件的方法(即将这些帖子和评论增加到ES服务器中,这个过程是异步的)。

(4)最后创建一个控制类SearchController,这里先调用service实现数据的搜索,再用一个map聚合数据。

List<Map<String, Object>> discussPosts = new ArrayList<>();
if (searchResult != null) {
            for (DiscussPost post : searchResult) {
                Map<String, Object> map = new HashMap<>();
                // 帖子
                map.put("post", post);
                // 作者
                map.put("user", userService.findUserById(post.getUserId()));
                // 点赞数量
                map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));

                discussPosts.add(map);
            }
        }

最后返回前端模板。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艺成超爱牛肉爆大虾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值