论坛项目功能总结【Java面试项目】

news2024/11/18 15:41:00

论坛项目功能总结【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

这篇博客能写好的原因是:站在巨人的肩膀上

这篇博客要写好的目的是:做别人的肩膀

开源:为爱发电

学习:为我而行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1683771.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32开发环境搭建Windows VSCode集成Espressif IDF插件开发环境搭建 IDF_V5.2.1

一、安装Visual Studio Code 下载地址&#xff1a;Download Visual Studio Code - Mac, Linux, Windows 打开上方链接&#xff0c;选择页面中的Windows版本&#xff0c;单击下载 将下载好的VSCodeUserSetup-x64-1.89.1.exe。单击右键&#xff0c;选择以管理员身份运行&#xf…

【Basic】BUU LFI COURSE

文章目录 前言一、BUU LFI COURSE二、知识点PHP的危险函数路径遍历攻击 解题感悟 前言 话不多说直接看题 一、BUU LFI COURSE emmm什么提示也没给啊&#xff0c;那只能点开看一看线索了 okok咱们先分析一下这段php代码 <?php /*** Created by PhpStorm.* User: jinzhao*…

R语言使用 ggscidca包优雅的绘制支持向量机决策曲线

DCA(Decision Curve Analysis)临床决策曲线是一种用于评价诊断模型诊断准确性的方法&#xff0c;在2006年由AndrewVickers博士创建&#xff0c;我们通常判断一个疾病喜欢使用ROC曲线的AUC值来判定模型的准确性&#xff0c;但ROC曲线通常是通过特异度和敏感度来评价&#xff0c;…

基于Nacos实现Sentinel规则持久化

基于Nacos实现Sentinel规则持久化 一、Sentinel使用痛点二、解决方案2.1 保存本地文件2.2 保存数据库2.3 保存到Nacos 三、规则持久化到Nacos3.1 Nacos服务端修改配置3.2 Sentinel控制台修改配置3.3 Nacos数据源整合到Sentinel中 一、Sentinel使用痛点 SpringCloudAlibaba帮我…

react 下拉框内容回显

需要实现效果如下 目前效果如下 思路 : 将下拉框选项的value和label一起存储到state中 , 初始化表单数据时 , 将faqType对应的label查找出来并设置到Form.Item中 , 最后修改useEffect 旧代码 //可以拿到faqType为0 但是却没有回显出下拉框的内容 我需要faqType为0 回显出下拉…

Laravel 11 PHP8

一直都是用laravel 7 左右的&#xff0c;现在要求将项目升级到laravel 11 和使用PHP8&#xff0c;随手记录一些小问题&#xff0c;laravel 11的包是领导给的&#xff0c;没有使用composer 安装&#xff0c;所以我也不确定和官方的是否一致 遇到这问题 可以这样 env 中默认的数…

基于Vue的验证码实现

一、验证码核心实现 创建slide-verify.vue&#xff0c;代码如下&#xff1a; <template><divclass"slide-verify":style"{ width: w px }"id"slideVerify"onselectstart"return false;"><!-- 图片加载遮蔽罩 -->&…

8操作系统定义、分类及功能+设备管理+作业管理 软设刷题 软考+

操作系统定义、分类及功能设备管理作业管理 知识点1-55-1010-1515-2020-2525-3030-35 刷题操作系统定义、分类及功能1-55-1010-15作业管理1-5设备管理1-55-10 知识点 1-5 1 嵌入式操作系统的特点&#xff1a; 1.微型化&#xff0c;从性能和成本角度考虑&#xff0c;希望占用的…

Android软件渲染流程

Android软件渲染流程 一.渲染流程1.VSync信号的监听2.VSync信号触发绘制 二.渲染原理1.画布的获取1.1 渲染缓存的初始化1.2 graphics::Canvas的创建1.3 graphics::Canvas与渲染缓存的绑定1.3.1 SkBitmap的初始化1.3.2 SkiaCanvas与SkBitmap的绑定1.3.3 SkCanvas的创建 2.矩形的…

【Day7:JAVA面向对象的初级使用】

目录 1、类和对象1.1 类的介绍1.2 类和对象的关系1.3 类的组成 2、对象内存图2.1 单个对象内存图2.2 两个对象内存图2.3 两个引用指向相同内存图 3、成员变量和局部变量3.1 成员变量和局部变量的区别 4、this关键字4.1 this可以解决的问题4.2 this介绍4.3 this内存图4.4 this总…

BOM..

区别&#xff1a;

验证码识别插件-captcha-killer

前言 想必大家都会使用burp进行爆破,当遇到带验证码的登录表单进行爆破时,基本尝试抓包后观察验证码是否主动更新,或者进行验证码绕过(我是十八期萌新,听风风说的有这个方法,但我还没学到),机缘巧合下我接触到了captcha-killer这个插件,可以提供给大家第三种爆破思路&#xff…

线程数据共享必学的3个工具类: ThreadLocal InheritableThreadLocal TransmittableThreadLocal

线程数据共享必学的3个工具类&#xff1a; ThreadLocal InheritableThreadLocal TransmittableThreadLocal 1.ThreadLocal:在当前线程中共享数据的&#xff0c;JUC 中提供的 2.InheritableThreadLocal:也是JUC中的一个工具类&#xff0c;解决 ThreadLocal 难以解决的问题 3.Tra…

MPLS原理与配置

1.MPLS概述 &#xff08;1&#xff09;传统IP路由转发 &#xff08;2&#xff09;MPLS基本概念 ⦁ MPLS起源于IPv4&#xff08;Internet Protocol version 4&#xff09;&#xff0c;其核心技术可扩展到多种网络协议&#xff0c;包括IPv6&#xff08;Internet Protocol ver…

WPF中CommandParameter用法

1. 界面样式 2. XAML中代码部分 <ButtonGrid.Row"0"Grid.Column"1"Command"{Binding BtnClick_Number}"CommandParameter"7"Content"7"Style"{StaticResource BtnStyle_Num}" /> <ButtonGrid.Row"…

我的第一个JAVA程序IDEA版

目录 第一步 新建一个空项目第二步 新建模块第三步 新建包第四步 新建类第五步 新建main方法 第一步 新建一个空项目 第二步 新建模块 第三步 新建包 第四步 新建类 然后在包文件夹下新建类 第五步 新建main方法

xlrd.biffh.XLRDError: Excel xlsx file; not supported报错原因

xlrd库读取xlsx文件时报错 xlrd.biffh.XLRDError: Excel xlsx file; not supported报错原因&#xff1a; xlrd版本为2.1版本&#xff0c;需要读取xlsx文件需要安装xlrd低一些版本1.2.0版本&#xff0c;重新安装重试即可 更换xlrd版本 重新运行

如何使用ffmpeg 实现10种特效

相关特效的名字 特效id 特效名 1 向上移动 2 向左移动 3 向下移动 4 颤抖 5 摇摆 6 雨刷 7 弹入 8 弹簧 9 轻微跳动 10 跳动 特效展示(同时汇总相关命令) pad背景显示 pad背景透明 相关命令(一会再讲这些命令&#xff0c;先往下看) # 合成特效语音 ffmpeg -y -loglevel erro…

【Linux】Linux信号产生,接受与处理机制

理解Linux信号产生&#xff0c;接受与处理机制 信号是Linux操作系统中一种用于进程间通信和异步事件处理的机制。在本文中&#xff0c;我们将结合Linux的源码&#xff0c;深入分析信号的产生、发送、接收和处理的底层原理。 文章目录 理解Linux信号产生&#xff0c;接受与处理…

Elasticsearch集群和Logstash、Kibana部署

1、 Elasticsearch集群部署 服务器 安装软件主机名IP地址系统版本配置ElasticsearchElk10.3.145.14centos7.5.18042核4GElasticsearchEs110.3.145.56centos7.5.18042核3GElasticsearchEs210.3.145.57centos7.5.18042核3G 软件版本&#xff1a;elasticsearch-7.13.2.tar.gz 示…