论坛项目功能总结【Java面试项目】
- 前言
- 推荐
- 项目功能总结
- 登录注册功能
- 注册概述
- 登录概述
- cookie-token实现
- redis实现
- 配置
- RedisUtil
- LoginController
- UserService
- 测试
- redis做缓存
- 帖子 DiscussPostController
- 发帖 addDiscussPost()
- 查看帖子详情 getDiscussPost()
- 置顶 setTop()
- 加精 setWonderful()
- 删除 setDelete()
- 评论 CommentController
- 私信 MessageController
- 得到私信列表 getLetterList()
- 得到私信详情 getLetterDetail()
- 发送私信 sendLetter()
- 发送私信
- 点赞 LikeController
- 点赞 like()
- 关注 FollowController
- 关注 follow()
- 取关 unfollow()
- 得到关注者 getFollowees()
- 得到粉丝 getFollowers()
- 私有方法 hasFollowed
- 通知 MessageController
- 得到通知列表 getNoticeList()
- 得到通知详情 getNoticeDetail()
- 搜索 SearchController
- 搜索 search()
- 网站数据统计 DataController
- 统计网站UV getUV()
- 统计活跃用户 getDAU()
- 热帖排行 PostScoreRefreshJob
- 执行刷新的方法
- 刷新帖子分数 refresh()
- HomeController
- getIndexPage()
- 事件生产者 EventProducer
- 处理事件 fireEvent()
- 事件消费者 EventConsumer
- 消费通知事件 handleCommentMessage()
- 消费发帖事件 handlePublishMessage()
- 消费删帖事件 handleDeleteMessage()
- 缓存
- DiscussPostService.init()
- UserService
- Util
- RedisKeyUtil
- 过滤算法:前缀树
- StringUtils
- CharUtils
- SensitiveFilter
- SensitiveTests
- 最后
前言
2023-5-9 20:09:10
2023-07-31 20:32:21
公开发布于
2024-5-20 13:11:40
以下内容源自【Java面试项目】
仅供学习交流使用
推荐
https://www.nowcoder.com/study/live/246
项目功能总结
主要使用了Springboot、Mybatis、MySQL、Redis、Kafka、等工具。
主要实现了用户的注册、登录、发帖、点赞、系统通知、按热度排序、搜索等功能。
另外引入了redis数据库来提升网站的整体性能,实现了用户凭证的存取、点赞关注的功能。
基于 Kafka 实现了系统通知:当用户获得点赞、评论后得到通知。
利用定时任务定期计算帖子的分数,并在页面上展现热帖排行榜。
登录注册功能
注册概述
三步:
- 通过表单提交数据。
- 服务端验证账号是否已存在、邮箱是否已注册。
- 服务端发送激活邮件。
通过表单提交数据(账号、密码、邮箱)
服务端处理register()
调用service层
空值处理 验证账号 验证邮箱
注册用户
密码使用md5盐值加密
激活码UUID
// http://localhost:8080/community/activation/101/code
发送激活邮件
返回操作(注册)-结果(成功)页面|返回注册页面
用户点击激活邮件
服务端调用activation()
调用service层
用户状态==1:重复激活
激活码相同:设置状态为1,激活成功
激活失败
跳转到操作-结果页面
登录概述
用户进入登录页面,就会请求后端,获取验证码
原来是把验证码存入session中
后面是存入cookie中,进而存入redis中
cookie(kaptchaOwner,UUID) (KaptchaKey_redisKey,验证码结果,60s)
用户填写信息:账号、密码、验证码
提交表单
后端就调用login方法
取出验证码的答案
原来是session中取出
后来是Cookie-->kaptchaOwner-->(Redis)kaptcha
验证码正确:下一步 ;否则返回错误信息
验证:用户名,密码
调用Service层
空值处理 验证账号 验证状态(用户的激活状态) 验证密码
如果都正确就生成登录凭证
原来的登录凭证是插入到一个数据库表中
后来是存入到redis中
通过Map返回错误信息,或者登录凭证
如果有登录凭证,就跳转到首页
如果没有,就返回错误信息
cookie-token实现
第2章 Spring Boot实践,开发社区登录模块
redis实现
第4章 Redis,一站式高性能存储方案
4.23 优化登陆模块 免费
配置
加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.2</version>
</dependency>
配置
# RedisProperties
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379
RedisUtil
package com.jsss.community.util;
/**
* 用来生成redis所用的key
*/
public class RedisKeyUtil {
private static final String SPLIT = ":";
private static final String PREFIX_KAPTCHA = "kaptcha";
private static final String PREFIX_TICKET = "ticket";
private static final String PREFIX_USER = "user";
// 登录验证码
// String owner 来标识访客 临时凭证在Cookie中获取
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
// 登录的凭证
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
// 用户
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
}
LoginController
redis替换session的功能
package com.jsss.community.controller;
@Controller
public class LoginController implements CommunityConstant{
private static final Logger logger= LoggerFactory.getLogger(LoginController.class);
@Autowired
UserService userService;
@Autowired
private Producer kaptchaProducer;
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/register")
public String register(Model model, User user){
}
// http://localhost:8080/community/activation/101/code
@RequestMapping(path = "/activation/{userId}/{code}",method = RequestMethod.GET)
public String activation(Model model, @PathVariable("userId") int userId,@PathVariable("code") String code){
}
@RequestMapping(path = "/kaptcha",method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session){
//生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//将验证码存入session
// session.setAttribute("kaptcha",text);
//验证码的归属者
String kaptchaOwner= CommunityUtil.generateUUID();
Cookie cookie=new Cookie("kaptchaOwner",kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入redis
String redisKey= RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);
//将图片输出给浏览器
response.setContentType("image/png");
try {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image,"png",os);
} catch (IOException e) {
logger.error("响应验证码失败:"+e.getMessage());
}
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, /*HttpSession session,*/ HttpServletResponse response,
@CookieValue("kaptchaOwner") String kaptchaOwner) {
//检查验证码
// String kaptcha = (String) session.getAttribute("kaptcha");
String kaptcha=null;
//Cookie-->kaptchaOwner-->(Redis)kaptcha
if (StringUtils.isNotBlank(kaptchaOwner)){
String redisKey=RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha= (String) redisTemplate.opsForValue().get(redisKey);
}
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "site/login";
}
// 检查账号,密码
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath(contextPath);
cookie.setMaxAge(expiredSeconds);
response.addCookie(cookie);
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "site/login";
}
}
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
}
UserService
package com.jsss.community.service;
@Service
public class UserService implements CommunityConstant {
@Autowired
UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
// @Autowired
// private LoginTicketMapper loginTicketMapper;
@Autowired
private RedisTemplate redisTemplate;
public User findUserById(int id){
return userMapper.selectById(id);
}
public Map<String,Object> register(User user){
}
public int activation(int userId,String code){
}
public Map<String,Object> login(String username,String password,int expiredSeconds){
Map<String,Object> map=new HashMap<>();
// 空值处理
if (StringUtils.isBlank(username)) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
// 验证账号
User user = userMapper.selectByName(username);
if (user == null) {
map.put("usernameMsg", "该账号不存在!");
return map;
}
// 验证状态
if (user.getStatus() == 0) {
map.put("usernameMsg", "该账号未激活!");
return map;
}
// 验证密码
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
map.put("passwordMsg", "密码不正确!");
return map;
}
// 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// loginTicketMapper.insertLoginTicket(loginTicket);
String redisKey= RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(redisKey,loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;
}
public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);
String redisKey= RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
redisTemplate.opsForValue().set(redisKey,loginTicket);
}
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket);
String redisKey= RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
public int updateHeader(int userId,String headUrl){
}
// 修改密码
public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) {
}
}
测试
启动redis
redis-server
访问:http://localhost:8080/community/login
获取验证码:LoginController.getKaptcha()
Cookie存入kaptchaOwner(UUID)
redis存入kaptchaOwner=验证码结果
输入信息
点击立即登录:LoginController.login()
通过Cookie取到kaptchaOwner
再用redis得到kaptchaOwner对应的值,即验证码结果
进入UserService.login()
把登录凭证存入redis
然后:把登录凭证放入cookie中
退出登录时,把登录凭证的状态设置为过期了
redis做缓存
更新操作:
更新数据的数据库与缓存的一致性:
先更新数据库,再删除缓存
package com.jsss.community.service;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class UserService implements CommunityConstant {
@Autowired
UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
// @Autowired
// private LoginTicketMapper loginTicketMapper;
@Autowired
private RedisTemplate redisTemplate;
public User findUserById(int id){
// return userMapper.selectById(id);
// User user=getCache(id);
// if (user==null){
// user = initCache(id);
// }
// return user;
return getCache(id)!=null?getCache(id):initCache(id);
}
public Map<String,Object> register(User user){
}
public int activation(int userId,String code){
}
public Map<String,Object> login(String username,String password,int expiredSeconds){
}
public void logout(String ticket) {
}
public LoginTicket findLoginTicket(String ticket) {
}
public int updateHeader(int userId,String headUrl){
// return userMapper.updateHeader(userId,headUrl);
int rows = userMapper.updateHeader(userId, headUrl);
clearCache(userId);
return rows;
}
public int updatePassword(int userId,String password){
// return userMapper.updatePassword(userId,password);
int rows = userMapper.updatePassword(userId,password);
clearCache(userId);
return rows;
}
public User findUserByName(String username) {
}
//1.优先从缓存中取值
private User getCache(int userId){
String redisKey=RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
//2.取不到初始化缓存数据
private User initCache(int userId){
User user = userMapper.selectById(userId);
String redisKey=RedisKeyUtil.getUserKey(userId);
// redisTemplate.opsForValue().set(redisKey,user,3600, TimeUnit.SECONDS);
//防止缓存雪崩,设置不同过期时间
//基础时间7200 +-3600 即 一小时到三小时之间
long time=7200+(new Random().nextInt(3600)-7200);
redisTemplate.opsForValue().set(redisKey,user,time, TimeUnit.SECONDS);
return user;
}
//3.数据变更时清除缓存数据
private void clearCache(int userId){
String redisKey=RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
}
帖子 DiscussPostController
发帖 addDiscussPost()
在MySQL中保存帖子
调用service层
转义HTML标记 过滤敏感词
触发发帖事件来进行系统通知,并且同步es中的数据
计算帖子分数
存入id到redis中,来进行排行
查看帖子详情 getDiscussPost()
帖子 作者 点赞数量 点赞状态
评论的分页信息
评论VO列表
作者 点赞数量 点赞状态 回复VO列表(作者 回复的目标 点赞数量 点赞状态)
回复的数量
置顶 setTop()
修改帖子的Type:1
触发发帖事件 es修改数据
加精 setWonderful()
修改帖子的status:1
触发发帖事件
计算帖子分数
redis中存入要刷分数的帖子id,来进行排行
删除 setDelete()
修改帖子的status:2
触发删帖事件,并且同步es中的数据
评论 CommentController
存储评论
调用service层
事务
添加评论 转义HTML标记 过滤敏感词
更新帖子评论数量
如果是回复的话(评论评论)
触发评论事件
如果是评论的话(评论帖子)
触发发帖事件
计算帖子分数
存入id到redis中,来进行排行
重定向到帖子详情
私信 MessageController
得到私信列表 getLetterList()
分页信息
会话列表(查询消息数量)
查询未读消息数量
得到私信详情 getLetterDetail()
分页信息
私信列表
私信目标
设置已读
得到消息的另外一个人
getLetterTarget
得到未读的消息ids
getLetterIds
发送私信 sendLetter()
发送私信
构建消息对象
ConversationId(小id_大id)
调用service层
转义HTML标记 过滤敏感词
点赞 LikeController
点赞 like()
点赞
调用service层
redis事务
添加|移除 用户
增加|减少 点赞数量
数量
状态
返回的结果(数量和状态)
触发点赞事件
如果是对帖子点赞
计算帖子分数
关注 FollowController
关注 follow()
调用service层
redis事务
add关注列表
add粉丝列表
触发关注事件
取关 unfollow()
调用service层
redis事务
remove关注列表
remove粉丝列表
得到关注者 getFollowees()
分页信息
查询关注者
得到粉丝 getFollowers()
分页信息
查询粉丝
私有方法 hasFollowed
返回用户是否关注了某个人
通知 MessageController
得到通知列表 getNoticeList()
查询评论类通知
查询点赞类通知
查询关注类通知
查询未读消息数量
得到通知详情 getNoticeDetail()
分页信息
通知信息(通知 内容 通知作者)
设置已读
搜索 SearchController
搜索 search()
搜索帖子
聚合数据 (帖子 作者 点赞数量)
分页信息
网站数据统计 DataController
统计网站UV getUV()
调用service层
整理该日期范围内的key
合并这些数据
union()
返回统计的结果
统计活跃用户 getDAU()
调用service层
整理该日期范围内的key
进行OR运算
OR bitCount
返回统计的结果
热帖排行 PostScoreRefreshJob
执行刷新的方法
判断Redis中用没有需要刷新的帖子id
执行刷新方法
刷新帖子分数 refresh()
是否精华
评论数量
点赞数量
计算权重=精华分+评论数×10 +点赞数×2
分数 = log10(max(帖子权重,1) + 距离天数
更新帖子分数
同步搜索数据
HomeController
按score来orderby排序
getIndexPage()
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page,
@RequestParam(name = "orderMode", defaultValue = "0") int orderMode) {
// 方法调用栈,SpringMVC会自动实例化Model和Page,并将Page注入Model.
// 所以,在thymeleaf中可以直接访问Page对象中的数据.
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index?orderMode=" + orderMode);
List<DiscussPost> list = discussPostService
.findDiscussPosts(0, page.getOffset(), page.getLimit(), orderMode);
List<Map<String, Object>> discussPosts = new ArrayList<>();
if (list != null) {
for (DiscussPost post : list) {
Map<String, Object> map = new HashMap<>();
map.put("post", post);
User user = userService.findUserById(post.getUserId());
map.put("user", user);
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts", discussPosts);
model.addAttribute("orderMode", orderMode);
//jar 报错thymleaf Error resolving template [/index],
// return "/index";
return "index";
}
DiscussPostService.findDiscussPosts()
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
if (userId == 0 && orderMode == 1) {
return postListCache.get(offset + ":" + limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
}
discusspost-mapper.xml
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status != 2
<if test="userId!=0">
and user_id = #{userId}
</if>
<if test="orderMode==0">
order by type desc, create_time desc
</if>
<if test="orderMode==1">
order by type desc, score desc, create_time desc
</if>
limit #{offset}, #{limit}
</select>
事件生产者 EventProducer
处理事件 fireEvent()
// 处理事件
public void fireEvent(Event event) {
// 将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
事件消费者 EventConsumer
消费通知事件 handleCommentMessage()
//消费通知事件
@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();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
//复用message conversation_id 做 topic
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
//拼接语句 用户nowcoder 评论了你的 帖子 , 点击查看 !
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
//附加信息 一律存入content
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
//复用message conversation_id 做 topic
//92,1,111,like,"{""entityType"":1,""entityId"":237,""postId"":237,""userId"":112}",1,2019-04-13 22:20:58
messageService.addMessage(message);
}
消费发帖事件 handlePublishMessage()
// 消费发帖事件
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(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;
}
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
消费删帖事件 handleDeleteMessage()
// 消费删帖事件
@KafkaListener(topics = {TOPIC_DELETE})
public void handleDeleteMessage(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;
}
elasticsearchService.deleteDiscussPost(event.getEntityId());
}
缓存
DiscussPostService.init()
@PostConstruct
public void init() {
// 初始化帖子列表缓存
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<String, List<DiscussPost>>() {
@Nullable
@Override
public List<DiscussPost> load(@NonNull String key) throws Exception {
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("参数错误!");
}
String[] params = key.split(":");
if (params == null || params.length != 2) {
throw new IllegalArgumentException("参数错误!");
}
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
// 二级缓存: Redis -> mysql
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
}
});
// 初始化帖子总数缓存
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Integer>() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
UserService
更新数据的数据库与缓存的一致性:
先更新数据库,再删除缓存
public int updateHeader(int userId,String headUrl){
// return userMapper.updateHeader(userId,headUrl);
int rows = userMapper.updateHeader(userId, headUrl);
clearCache(userId);
return rows;
}
public int updatePassword(int userId,String password){
// return userMapper.updatePassword(userId,password);
int rows = userMapper.updatePassword(userId,password);
clearCache(userId);
return rows;
}
私有方法
//1.优先从缓存中取值
private User getCache(int userId){
String redisKey=RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
//2.取不到初始化缓存数据
private User initCache(int userId){
User user = userMapper.selectById(userId);
String redisKey=RedisKeyUtil.getUserKey(userId);
// redisTemplate.opsForValue().set(redisKey,user,3600, TimeUnit.SECONDS);
//防止缓存雪崩,设置不同过期时间
//基础时间7200 +-3600 即 一小时到三小时之间
long time=7200+(new Random().nextInt(3600)-7200);
redisTemplate.opsForValue().set(redisKey,user,time, TimeUnit.SECONDS);
return user;
}
//3.数据变更时清除缓存数据
private void clearCache(int userId){
String redisKey=RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
Util
RedisKeyUtil
package com.jsss.community.util;
/**
* 用来生成redis所用的key
*/
public class RedisKeyUtil {
private static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
private static final String PREFIX_USER_LIKE = "like:user";
private static final String PREFIX_FOLLOWEE = "followee";
private static final String PREFIX_FOLLOWER = "follower";
private static final String PREFIX_KAPTCHA = "kaptcha";
private static final String PREFIX_TICKET = "ticket";
private static final String PREFIX_USER = "user";
private static final String PREFIX_UV = "uv";
private static final String PREFIX_DAU = "dau";
private static final String PREFIX_POST = "post";
// 某个实体的赞
// value 设置set类型 某个userId对其的赞,统计数量就是赞的数量
// like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
// 某个用户的赞
// like:user:userId -> int
public static String getUserLikeKey(int userId) {
return PREFIX_USER_LIKE + SPLIT + userId;
}
// 某个用户关注的实体
// followee:userId:entityType -> zset(entityId,now)
public static String getFolloweeKey(int userId, int entityType) {
return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}
// 某个实体拥有的粉丝
// follower:entityType:entityId -> zset(userId,now)
public static String getFollowerKey(int entityType, int entityId) {
return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}
// 登录验证码
// String owner 来标识访客 临时凭证在Cookie中获取
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
// 登录的凭证
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
// 用户
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
// 单日UV
public static String getUVKey(String date) {
return PREFIX_UV + SPLIT + date;
}
// 区间UV
public static String getUVKey(String startDate, String endDate) {
return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
}
// 单日活跃用户
public static String getDAUKey(String date) {
return PREFIX_DAU + SPLIT + date;
}
// 区间活跃用户
public static String getDAUKey(String startDate, String endDate) {
return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
}
// 帖子分数
public static String getPostScoreKey() {
return PREFIX_POST + SPLIT + "score";
}
}
过滤算法:前缀树
在Java普通项目中测试
StringUtils
package community;
public class StringUtils {
public static boolean isBlank(final CharSequence cs) {
final int strLen = length(cs);
if (strLen == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
public static int length(final CharSequence cs) {
return cs == null ? 0 : cs.length();
}
}
CharUtils
package community;
public class CharUtils {
public static boolean isAsciiAlphanumeric(final char ch) {
return isAsciiAlpha(ch) || isAsciiNumeric(ch);
}
public static boolean isAsciiAlpha(final char ch) {
return isAsciiAlphaUpper(ch) || isAsciiAlphaLower(ch);
}
public static boolean isAsciiAlphaUpper(final char ch) {
return ch >= 'A' && ch <= 'Z';
}
public static boolean isAsciiAlphaLower(final char ch) {
return ch >= 'a' && ch <= 'z';
}
public static boolean isAsciiNumeric(final char ch) {
return ch >= '0' && ch <= '9';
}
}
SensitiveFilter
package community;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class SensitiveFilter {
// 替换符
private static final String REPLACEMENT="***";
//根节点
private TrieNode rootNode=new TrieNode();
public void init(){
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
){
String keyword;
while ((keyword = reader.readLine())!=null){
//添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
System.out.println("加载敏感词文件失败"+e.getMessage());
}
}
//将一个敏感词添加到前缀树中
private void addKeyword(String keyword) {
TrieNode tempNode=rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode==null){
//初始化子节点
subNode=new TrieNode();
tempNode.addSubNode(c,subNode);
}
//指针指向子节点,进入下一轮循环
tempNode=subNode;
//设置结束表示
if (i==keyword.length()-1){
tempNode.setKeyWordEnd(true);
}
}
}
/**
* 过滤敏感词
* @param text 待过滤的文本
* @return 过滤后的文本
*/
public String filter(String text){
if (StringUtils.isBlank(text)){
return null;
}
//指针1
TrieNode tempNode = rootNode;
//指针2
int begin=0;
//指针3
int position=0;
//结果
StringBuilder sb=new StringBuilder();
while (position<text.length()){
char c=text.charAt(position);
//跳过符号☆
if (isSymbol(c)){
//若指针1处于根节点,将符号计入结果,让指针2向下走一步
if(tempNode==rootNode){
sb.append(c);
begin++;
}
//无论符号在开头或中间,指针3都向下走一步
position++;
continue;
}
//检查下级节点
tempNode=tempNode.getSubNode(c);
if (tempNode==null){
//以begin开头的字符不是敏感词
sb.append(text.charAt(begin));
//进入下一个位置
position=++begin;
//重新指向根节点
tempNode=rootNode;
}else if (tempNode.isKeyWordEnd()){
//发现敏感词,将begin~position字符串替换掉
sb.append(REPLACEMENT);
//进入下一个位置
begin=++position;
//重新指向根节点
tempNode=rootNode;
}else {
//检查下一个字符
++position;
}
}
//将最后一批字符计入结果,当指针3提前结束
sb.append(text.substring(begin));
return sb.toString();
}
//判断是否为符号
private boolean isSymbol(Character c){
//0x2E80~0x9FFF 东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) &&(c<0x2E80||c>0x9FFF);
}
//前缀树
private class TrieNode{
//关键词结束标识
private boolean keyWordEnd =false;
//子节点(key是下级字符,value是下级结点)
private Map<Character,TrieNode> subNodes =new HashMap<>();
public TrieNode() {
}
public void setKeyWordEnd(boolean keyWordEnd) {
this.keyWordEnd = keyWordEnd;
}
public boolean isKeyWordEnd() {
return keyWordEnd;
}
//添加子节点
public void addSubNode(Character c,TrieNode node){
subNodes.put(c,node);
}
//获取子节点
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
SensitiveTests
package community;
public class SensitiveTests {
public void testSensitiveFilter(){
SensitiveFilter sensitiveFilter=new SensitiveFilter();
sensitiveFilter.init();
String text="这里可以唱跳,可以Rap,可以篮球,哈哈哈!";
String filter = sensitiveFilter.filter(text);
System.out.println(filter);
// 这里可以***,可以***,可以***,哈哈哈!
}
public static void main(String[] args) {
new SensitiveTests().testSensitiveFilter();
}
}
最后
2023-5-9 21:58:25
2023-7-26 19:55:54
这篇博客能写好的原因是:站在巨人的肩膀上
这篇博客要写好的目的是:做别人的肩膀
开源:为爱发电
学习:为我而行