牛客网Java实战项目--仿牛客网社区的学习笔记

仿牛客网社区的学习笔记

  • 1. 项目环境搭建
    • 1.1 开发社区首页
  • 2.开发社区登录模块
    • 2.1 发送邮件
    • 2.2 开发注册功能
    • 2.3 会话管理
    • 2.4 生成验证码
    • 2.5 开发登录、退出功能
    • 2.6 显示登录信息
  • 4 Redis实现点赞关注
    • 4.1 Spring整合Redis
      • 访问Redis的方法:
    • 4.2 Redis实现点赞
      • 4.2.1 点赞
      • 4.2.2 用户收到的赞
    • 4.3 Redis实现关注
      • 4.3.1 实现关注、取关
      • 4.3.2 关注列表,粉丝列表
    • 4.4 优化登录模块
      • 4.4.1 使用Redis存储验证码
      • 4.4.2 使用Redis存储登录凭证
      • 4.4.3 使用Redis缓存用户信息

项目链接: 仿牛客网社区开发.

1. 项目环境搭建

• 构建工具:Apache Maven
• 集成开发工具:IntelliJ IDEA
• 数据库:MySQL、Redis
• 应用服务器:Apache Tomcat
• 版本控制工具:Git
Spring Boot功能:起步依赖,自动配置,断点监控。

1.1 开发社区首页

首先设计数据库表:

DROP TABLE IF EXISTS `discuss_post`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE `discuss_post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(45) DEFAULT NULL,
  `title` varchar(100) DEFAULT NULL,
  `content` text,
  `type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;',
  `status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;',
  `create_time` timestamp NULL DEFAULT NULL,
  `comment_count` int(11) DEFAULT NULL,
  `score` double DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后创建实体类和表结构对应,并产生getter和setter方法,toString方法。

@Document(indexName = "discusspost",type = "_doc",shards = 6,replicas = 3)
public class DiscussPost {

    @Id
    private int id;

    @Field(type = FieldType.Integer)
    private int userId;

    @Field(type = FieldType.Text,analyzer = "ik_max_work",searchAnalyzer = "ik_smart")
    private String title;

    @Field(type = FieldType.Text,analyzer = "ik_max_work",searchAnalyzer = "ik_smart")
    private String content;

    @Field(type = FieldType.Integer)
    private int type;

    @Field(type = FieldType.Integer)
    private int status;

    @Field(type = FieldType.Date)
    private Date createTime;

    @Field(type = FieldType.Integer)
    private int commentCount;

    @Field(type = FieldType.Double)
    private double score;

}

一次请求,服务器响应后显示前十个帖子,并开发分页组件,实现跳转不同分页显示不同帖子。当浏览器访问根目录后,会进行forward转发到/index,然后查询贴子,用户信息,通过model携带,再交给index.html页面进行显示。

    @RequestMapping(path = "/", method = RequestMethod.GET)
    public String root(){
        return "forward:/index";
    }
    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model, Page page,@RequestParam(name = "orderMode",defaultValue = "0") int orderMode){
        //方法调用之前,springmvc自动实例化Page,且注入给Model,thymeleaf可以直接访问Page
        page.setRows(discussPostService.findDiscussPostRows(0));
        page.setPath("/index?orderMode=" + orderMode);
        List<DiscussPost> list = discussPostService.findDiscussPost(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);
        return "index";
    }

在这里插入图片描述
首页效果展示:
在这里插入图片描述

2.开发社区登录模块

2.1 发送邮件

  • 邮箱设置

以新浪邮箱为例,开启SMTP服务。
在这里插入图片描述

  • Spring Email

导入jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>2.6.2</version>
</dependency>

邮箱参数配置,添加在配置文件中,与程序解耦
注意现在新浪邮箱改版了,密码需要输入授权码,在上图中点击开启授权码即可

spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=你的邮箱
spring.mail.password=你的密码
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true

在这里插入图片描述
使用JavaMailSender发送邮件

@Component
public class MailClient {

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

    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    public void sendMail(String to, String subject,String content) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content,true);
            mailSender.send(helper.getMimeMessage());
        } catch (MessagingException e) {
            logger.error("发送邮件失败"+e.getMessage());
        }
    }
}
  • 模板引擎
      利用Thymeleaf模板引擎发送HTML邮件,注入模板引擎TemplateEngine,利用setVriable设置要传给模板引擎的参数,process指定html文件放在哪里,最后sendMail。
@Autowired
private TemplateEngine templateEngine;

@Test
public void testHtmlMail(){
    Context context = new Context();
    context.setVariable("username","monday");
    String content = templateEngine.process("/mail/demo", context);
    System.out.println(content);
    mailClient.sendMail("***@sina.com","HTML",content);
}

2.2 开发注册功能

  • 访问注册页面
@RequestMapping(path = "/register", method = RequestMethod.GET)
public String getRegisterPage(){
    return "/site/register";
}

在这里插入图片描述

  • 提交注册数据

先导入commons lang包,有些和字符串判断的工具类后面会经常用到

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

把域名配置好,因为邮件的链接中需要用到,以及上传文件的位置也可以配置好

community.path.domain=http://localhost:8080
community.path.upload=e:/data/upload

service层代码如下,需要判断账号密码是否为空,账号邮箱是否已存在,也可以加上对密码形式的限制。如果格式合格,再把用户数据写到数据库中,并发送一封激活邮件到用户邮箱中,用户点击邮箱中的激活链接就能激活并正常使用账号了。

    public Map<String ,Object> register(User user){
        Map<String ,Object> map = new HashMap<>();
        //空值处理
        if(user==null){
            throw new IllegalArgumentException("参数不能为空!");
        }
        if(StringUtils.isBlank(user.getUserName())){
            map.put("usernameMsg","账号不能为空!");
            return map;
        }
        if(StringUtils.isBlank(user.getPassword())){
            map.put("passwordMsg","密码不能为空!");
            return map;
        }
        if(StringUtils.isBlank(user.getEmail())){
            map.put("emailMsg","邮箱不能为空!");
            return map;
        }
        //验证是否可以注册
        User u = userMapper.selectByName(user.getUserName());
        if(u!=null){
            map.put("usernameMsg","该账号已存在!");
            return map;
        }
        u = userMapper.selectByEmail(user.getEmail());
        if(u!=null){
            map.put("emailMsg","该邮箱已被注册!");
            return map;
        }
        //注册用户
        user.setSalt(CommunityUtil.generateUUID().substring(0,5));
        user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));
        user.setType(0);
        user.setStatus(0);
        user.setActivationCode(CommunityUtil.generateUUID());
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));
        user.setCreateTime(new Date());
        //添加到数据库
        userMapper.insertUser(user);
        //发邮件
        Context context = new Context();
        context.setVariable("email",user.getEmail());
        String url = domain + contextPath + "/activation/" + user.getId() +"/" + user.getActivationCode();
        context.setVariable("url",url);
        String content = templateEngine.process("/mail/activation",context);
        mailClient.sendMail(user.getEmail(),"激活账号",content);
        return map;
    }

controller层代码如下,如果service传过来的map不空则用model携带错误信息,并重定向到注册页面,如果map为空,用model携带注册成功信息,重定向到操作结果页面,几秒后跳转,提示用户激活邮件。

    @RequestMapping(path = "/register", method = RequestMethod.POST)
    public String register(Model model, User user){
        Map<String, Object> map = userService.register(user);
        if(map==null||map.isEmpty()){
            model.addAttribute("msg","注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");
            model.addAttribute("target","/index");
            return "/site/operate-result";
        }
        else{
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("passwordMsg",map.get("passwordMsg"));
            model.addAttribute("emailMsg",map.get("emailMsg"));
            return "/site/register";
        }
    }
  • 激活注册账号

用户点击激活链接后访问服务器,服务器取到路径中的激活码和用户ID,和数据库中进行对比,如果对比成功则激活成功。
在接口CommunityConst中定义一些常量,表示激活的状态:

    int ACTIVATION_SUCCESS = 0;

    int ACTIVATION_REPEAT = 1;

    int ACTIVATION_FAIL = 2;

service层代码如下,利用controller传过来的用户ID和激活码在数据库中查询是否存在,如果查询不到,则激活失败,如果查询到了,再根据激活状态status判断是否重复激活或者是激活成功,将激活状态返回到controller。

    public int activation(int userId, String code){
        User user = userMapper.selectById(userId);
        if(user.getStatus()==1){
            return ACTIVATION_REPEAT;
        }
        else if(user.getActivationCode().equals(code)){
            userMapper.updateStatus(userId,1);
            //数据更新,清除缓存
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        }
        else{
            return ACTIVATION_FAIL;
        }
    }

controller层代码如下,利用PathVariable注解获得路径中的参数,然后根据service层传过来的结果判断是哪种情况,并利用model携带相关信息,重定向到操作结果页面。

    @RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET)
    public String activation(Model model, @PathVariable("userId") int userId,@PathVariable("code") String code){
        int result = userService.activation(userId, code);
        if(result==ACTIVATION_SUCCESS){
            model.addAttribute("msg","激活成功,您的账号已经可以正常使用了!");
            model.addAttribute("target","/login");
        }
        else if(result==ACTIVATION_REPEAT){
            model.addAttribute("msg","无效操作,该账号已经激活过!");
            model.addAttribute("target","/index");
        }
        else{
            model.addAttribute("msg","激活失败,您提供的激活码不正确");
            model.addAttribute("target","/index");
        }
        return "/site/operate-result";

激活效果如下:
在这里插入图片描述

2.3 会话管理

  为什么需要使用会话管理呢?因为HTTP协议是无状态的,有会话的,如果不同请求之间有联系,我们可以通过会话技术使得用户感觉不同请求之间是有记忆性的。常用会话技术有Cookie和Session,Cookie是保存在用户浏览器缓存中的,可以设置过期时间,每次浏览器访问服务器都会把Cookie携带过来;Session是保存在服务器内存中的,一般重要的消息防止黑客通过用户浏览器盗取,会放在Session中,但过多的Session也会增大服务器的压力,注意Session的实现依赖于Cookie,需要携带一个SessionId才能获取相应的Session。
  面试题:分布式部署时使用Session会有什么问题?怎么解决?
分布式部署,不同服务器之间存的Session不同,下次某个浏览器访问的服务器可能不存在需要的Session
1、可以同步Session,但是每一个服务器都保存一份一样的Session则浪费内存资源,影响性能,并且服务器之间会产生耦合,并不是独立的
2、可以设置粘性Session,只要是相同IP,就分给同一个服务器处理,但这样缺点就是负载不能均衡,不能有效利用分布式部署的优点。
3、可以考虑单独设置一台服务器管理Session,其他服务器需要查询Session时访问这个服务器,但是万一这个服务器挂了,也是一个瓶颈,如果也使用集群,其实和之前没什么区别了
4、更通用的解决方法是,能存到Cookie就存到Cookie中,如果不能存到Cookie就存到数据库中,数据库是可以做集群的,技术也比较成熟,但是传统数据库依靠硬盘存储数据,读取比较慢,可以考虑会话数据存到非关系型数据库中,比如Redis

2.4 生成验证码

  • 导入jar包
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
  • 编写Kaptcha配置类
@Configuration
public class KaptchaConfig {

    @Bean
    public Producer kaptchaProducer(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width","100");
        properties.setProperty("kaptcha.image.height","40");
        properties.setProperty("kaptcha.textproducer.font.size","32");
        properties.setProperty("kaptcha.textproducer.font.color","0,0,0");
        properties.setProperty("kaptcha.textproducer.char.string","0123456789ABCDEFGHIJKLNOPQRSTUVWXYZ");
        properties.setProperty("kaptcha.textproducer.char.length","4");
        properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}
  • 生成随机字符、生成随机图片

先用刚刚配置好的kaptchaProducer产生随机字符串,并根据字符串产生图片,这里还没有用到Redis,先把字符串存到Session中,由于这时候用户还没有登录,不能根据用户ID等信息去鉴别是哪个用户,这个时候可以生成随机字符串当作临时凭证,存在Cookie中,设置最大过期时间60s,下次再访问服务器就能携带过来登录凭证。获得response的输出流,把图片输到输出流中,而且不用我们手动关闭资源,Spring会自动关闭response的输出流。

    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response/*, HttpSession session*/){
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);
        session.setAttribute("kaptcha",text);
        //验证码的归属
        String kaptchaOwner = CommunityUtil.generateUUID();
        Cookie cookie = new Cookie("kaptchaOwner",kaptchaOwner);
        cookie.setMaxAge(60);
        cookie.setPath(contextPath);
        response.addCookie(cookie);
        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image,"png",os);
        } catch (IOException e) {
            logger.error("响应验证码失败" + e.getMessage());
        }

    }

验证码图片效果如下:
在这里插入图片描述

2.5 开发登录、退出功能

  • 登录

浏览器发送用户名,密码,验证码到服务器,服务器判断是否都符合,如果符合则生成登陆凭证,下次根据登陆凭证就能自动登录,而不用重复登录,如果不符合,则跳转回登陆页面。数据库中的表login_ticket和LoginTicket对应。ticket是登陆凭证,status是登陆状态,expired是登录过期时间。
在这里插入图片描述
dao层的接口代码如下,这里演示用注解方式写sql语句,实现的函数有插入登录登陆凭证,查询登录凭证,更新登录凭证,这里的登录凭证是指包含的login_ticket全部字段。注意动态sql要放在script标签内。

@Mapper
public interface LoginTicketMapper {
    @Insert({
            "insert into login_ticket (user_id,ticket,status,expired) ",
            "values(#{userId},#{ticket},#{status},#{expired})"
    })
    @Options(useGeneratedKeys = true,keyProperty = "id")
    int insertLoginTicket(LoginTicket loginTicket);
    //以ticket为条件
    @Select({
            "select id,user_id,ticket,status,expired from login_ticket ",
            "where ticket = #{ticket}"
    })
    LoginTicket selectByTicket(String ticket);
    //演示注解中写动态sql,前后要有script标签
    @Update({
            "<script>",
            "update login_ticket set status = #{status} where ticket = #{ticket} ",
            "<if test=\"ticket!=null\">",
            "and 1=1",
            "</if>",
            "</script>"
    })
    int updateStatus(String ticket, int status);
}

service层的代码如下,首先判断账号密码是否为空,账号是否激活,是否存在,密码是否正确,都通过后再生成登陆凭证,并存到数据库中,并在map中携带登陆凭证返回给controlller。

    public Map<String ,Object> login(String userName, String password, long 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(!password.equals(user.getPassword())){
            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);

        map.put("ticket",loginTicket.getTicket());
        return map;
    }

controller层代码如下,kan1验证码是否正确,根据用户是否勾选记住我来设置过期时间,如果map中有ticket,将ticket存入Cookie中。失败时携带错误信息,跳转到登陆页面。

    @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");
        if(StringUtils.isBlank(kaptcha)||StringUtils.isBlank(code)||!kaptcha.equalsIgnoreCase(code)){
            model.addAttribute("codeMsg","验证码不正确");
            return "/site/login";
        }
        int expiredSeconds = rememberMe?REMEMBER_EXPIRED_SECOND:DEFAULT_EXPIRED_SECOND;
        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";
        }

    }
  • 退出

将登陆状态改为失效状态,跳转回网站首页
service层的代码如下,更新登陆凭证的状态,

    public void logout(String ticket){
        loginTicketMapper.updateStatus(ticket,1);
        redisTemplate.opsForValue().set(redisKey,loginTicket);
    }

controller层代码如下,利用注解CookieValue获得键为ticket的Cookie,使用业务层的登出方法,重定向到登陆页面。

    @RequestMapping(path = "/logout",method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket){
        userService.logout(ticket);
        SecurityContextHolder.clearContext();
        return "redirect:/login";
    }

2.6 显示登录信息

  • 拦截器

4 Redis实现点赞关注

  Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。

4.1 Spring整合Redis

引入依赖,version可以不写,因为继承的父pom会声明好版本。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis
配置数据库参数,在Application.properties配置
从图中可以看到Spring Boot配置的为RedisAutoConfiguration,配置的是RedisProperties类,因此我们也配置RedisProperties。
在这里插入图片描述
配置含义:Redis默认数据库有16个,选择第11个使用,端口为6379,默认为此端口。

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

编写配置类,构造RedisTemplate
Spring Boot已经配好了RedisTemplate,但是Key是Object类型,虽然更通用,但是我们用String作为Key,因此重新配置一下。首先加上注解@Configuration,让Spring知道这是一个配置类,然后重写@Bean的RedisTemplate,Key是String,Value是Object。形参需要Spring注入连接工厂RedisConnectionFactory,这里使用了工厂模式。当定义Bean时,声明形参,Spring会将它注入进来。实例化RedisTemplate,将工厂给template,具有访问数据库的能力。然后指定数据的序列化方式,依次指定Key和Value序列化方式,而Hash比较特殊,它的值又包含Key和Value,因此要设置Hash的Value的Key和Value。设置后afterPropertiesSet触发生效,并将template返回。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String ,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
    //    设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
    //    设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
    //    设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
    //    设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());
        //生效
        template.afterPropertiesSet();
        return template;
    }
}

访问Redis的方法:

  • redisTemplate.opsForValue()
  • redisTemplate.opsForHash()
  • redisTemplate.opsForList()
  • redisTemplate.opsForSet()
  • redisTemplate.opsForZSet()

4.2 Redis实现点赞

点赞需要实现的功能:对评论,帖子都可以点赞,第一次点赞,第二次会取消点赞。首页统计点赞数量,每个帖子详情页也要统计点赞数量和用户点赞状态。

4.2.1 点赞

首先对于频繁获取Key,我们可以考虑写一个工具类RedisKeyUtil将这个功能进行提取:

public class RedisKeyUtil {

    private static final String SPLIT = ":";
    private static final String PREFIX_ENTITY_LIKE = "like:entity";
//    生成某个实体的赞
//    like:entity:entityType:entityId -> set 看谁赞了,set里装userId
    public static String getEntityLikeKey(int entityType, int entityId){
        return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
    }
}

编写业务层LikeService,将RedisTemplate注入,like方法形参传进来用户ID,实体类型是帖子或者评论,以及实体ID,这里考虑了事务。不考虑事务的话,先通过isMember判断键值对是否在Redis中存在,如果存在则remove,不存在则add。
findEntityLikeCount方法统计点赞数量,findEntityLikeStatus统计点赞状态。

@Service
public class LikeService {

    @Autowired
    private RedisTemplate redisTemplate;
    
    public void like(int userId,int entityType, int entityId){
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType,entityId);
        boolean isMember = operations.opsForSet().isMember(entityLikeKey,userId);
        if (isMember){
            redisTemplate.opsForSet().remove(entityLikeKey,userId);
        }
        else{
            redisTemplate.opsForSet().add(entityLikeKey,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;
    }
}

编写表现层LikeController,使用HostHolder获得当前用户,从而获得用户ID,调用业务层的like方法点赞,并统计实体的点赞数量,用户对实体的点赞状态,将这两个信息装到map中。最后返回JSON格式的数据,状态码为0,带上map。

@Controller
public class LikeController implements CommunityConstant {

    @Autowired
    private LikeService likeService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(path = "/like",method = RequestMethod.POST)
    @ResponseBody
    public String like(int entityType, int entityId,int entityUserId,int postId){
        User user = hostHolder.getUser();
        likeService.like(user.getId(),entityType,entityId,entityUserId);
        long likeCount = likeService.findEntityLikeCount(entityType, entityId);
        int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
        Map<String ,Object> map = new HashMap<>();
        map.put("likeCount",likeCount);
        map.put("likeStatus",likeStatus);
        return CommunityUtil.getJSONString(0,null,map);
    }
}

在discuss-detail.html修改点赞,使用AJAX异步请求,绑定onclick,一旦点击使用javascript方法like进行点赞。发送post请求提交数据,访问路径为CONTEXT_PATH加上/like,携带上JSON数据,定义响应函数,把返回的数据解析成JSON字符串,如果状态码为0,则正常返回,将赞状态进行修改,并修改点赞数量。如果状态码不是0,弹出携带的错误信息。

function like(btn,entityType,entityId,entityUserId,postId){
    $.post(
        CONTEXT_PATH + "/like",
        {"entityType":entityType,"entityId":entityId,"entityUserId":entityUserId,"postId":postId},
        function (data){
            data = $.parseJSON(data);
            if(data.code==0){
                $(btn).children("b").text(data.likeStatus==1?"已赞":"赞");
                $(btn).children("i").text(data.likeCount);
            }
            else{
                alert(data.msg);
            }
        }
    );
}

此时还存在问题,主页刚打开时并未点赞,点赞数量没有及时更新,因此我们需要在HomeController里加上统计点赞数的功能。

long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,post.getId());
map.put("likeCount",likeCount);

帖子详情页面刚打开时也需要统计点赞数,因此在DiscussPostController加上功能,注意这里复杂一些,首先需要统计帖子点赞数和点赞状态,然后在评论区和回复区也要统计点赞数和点赞状态。最后修改html页面。
效果展示:
在这里插入图片描述

4.2.2 用户收到的赞

这时候需要重构点赞功能,在点赞时使用incement,取消赞时使用decrement。这样虽然数据有一些冗余(因此通过用户id也能查处总赞数),但是换来查询用户赞数的时间大大缩短。
添加Key和获取Key的方法

private static final String PREFIX_USER_LIKE = "like:user";
//    某一个用户的赞
//    like:user:userId -> int
    public static String getUserLikeKey(int userId){
        return PREFIX_USER_LIKE + SPLIT + userId;
    }

需要对like方法进行修改,使用编程式事务,修改后如下,增加查询用户赞数方法findUserLikeCount:

//重构,因为要执行两次操作,因此要使用编程式事务
public void like(int userId,int entityType, int entityId,int entityUserId){
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType,entityId);
            String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
            boolean isMember = operations.opsForSet().isMember(entityLikeKey,userId);
            operations.multi();
            if (isMember){
                operations.opsForSet().remove(entityLikeKey,userId);
                operations.opsForValue().decrement(userLikeKey);
            }
            else{
                operations.opsForSet().add(entityLikeKey,userId);
                operations.opsForValue().increment(userLikeKey);
            }
            return operations.exec();
        }
    });
}

//查询某个用户获得的赞数
public int findUserLikeCount(int userId){
    String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
    Integer userLikeCount = (Integer) redisTemplate.opsForValue().get(userLikeKey);
    return userLikeCount==null?0:userLikeCount.intValue();
}

接下来重构表现层LikeController,增加entityUserId,like方法增加进去即可。

likeService.like(user.getId(),entityType,entityId,entityUserId);

修改html页面。
在UserController中,追加更新赞数,并且用户主页自己可以访问,其他用户也能访问。需要页面传过来用户ID。使用@PathVariable注解,将yonghuID解析。查出用户,如果没有则抛出异常,如果有则在model中携带用户和查询的点赞数。返回到profile.html,进行修改动态显示数据。

@RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model){
    User user = userService.findUserById(userId);
    if (user==null){
        throw new RuntimeException("该用户不存在!");
    }
    model.addAttribute("user",user);
    int likeCount = likeService.findUserLikeCount(userId);
    model.addAttribute("likeCount",likeCount);
    return "/site/profile";
}

在这里插入图片描述

4.3 Redis实现关注

4.3.1 实现关注、取关

开发关注功能,需要关注,取消关注,统计用户关注了几个人,被几个人关注。这里粉丝用Follower,被关注者用Followee。被关注的目标可以是用户,帖子,题目等,这些都叫做实体entity,通过entityType来区分具体哪个实体。1是帖子,2是评论,3是用户。
在RedisKeyUtil添加的常量和方法,第一个方法某用户关注的实体,Key包含关注者ID,关注的类型,而后面加的Value是具体关注的实体ID,第二个方法是实体的关注数,Key包含实体类型,实体ID,Value为关注数。使用Zset能进行排序,根据时间排序,使用now。

    private static final String PREFIX_FOLLOWER = "follower";
    private static final String PREFIX_FOLLOWEE = "followee";
    
//    某个用户关注的实体
//    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;
    }

编写业务层FollowService,使用编程式事务,分别构造两个Key,进行两次添加。包含关注和取关两个功能。

@Service
public class FollowService implements CommunityConstant {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;

    //关注
    public void follow(int userId, int entityType, int entityId) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
                operations.multi();
                operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
                operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
                return operations.exec();
            }
        });
    }
    //取关
    public void unfollow(int userId, int entityType, int entityId) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
                operations.multi();
                operations.opsForZSet().remove(followeeKey, entityId);
                operations.opsForZSet().remove(followerKey, userId);
                return operations.exec();
            }
        });
    }
}

编写表现层FollowController,

@Controller
public class FollowController implements CommunityConstant{

    @Autowired
    private FollowService followService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @Autowired
    private EventProducer eventProducer;

    @RequestMapping(path = "/follow",method = RequestMethod.POST)
    @ResponseBody
    public String  follow(int entityType, int entityId){
        User user = hostHolder.getUser();
        followService.follow(user.getId(),entityType,entityId);
        return CommunityUtil.getJSONString(0,"关注成功!");
    }
    
    @RequestMapping(path = "/unfollow",method = RequestMethod.POST)
    @ResponseBody
    public String  unfollow(int entityType, int entityId){
        User user = hostHolder.getUser();
        followService.unfollow(user.getId(),entityType,entityId);
        return CommunityUtil.getJSONString(0,"取消关注成功!");
    }

接下来修改页面,有许多地方都要求有关注功能,比如用户主页可以被其他用户关注,帖子详情页可以被关注等,需要一一修改。

function follow() {
	var btn = this;
	if($(btn).hasClass("btn-info")) {
		// 关注TA
		$.post(
			CONTEXT_PATH + "/follow",
			{"entityType":3,"entityId":$(btn).prev().val()},
			function (data){
				data = $.parseJSON(data);
				if(data.code==0){
					window.location.reload();
				}
				else{
					alert(data.msg);
				}
			}
		);
		// $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary");
	} else {
		$.post(
			CONTEXT_PATH + "/unfollow",
			{"entityType":3,"entityId":$(btn).prev().val()},
			function (data){
				data = $.parseJSON(data);
				if(data.code==0){
					window.location.reload();
				}
				else{
					alert(data.msg);
				}
			}
		);
		// 取消关注
		// $(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
	}
}

然后实现查询关注数量,被关注数量,编写FollowService,这里统计zset数量使用zCard方法。

    //    查询关注的实体的数量
    public long findFolloweeCount(int userId, int entityType) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
        return redisTemplate.opsForZSet().zCard(followeeKey);
    }
    //查询粉丝数
    public long findFollowerCount(int entityType, int entityId) {
        String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
        return redisTemplate.opsForZSet().zCard(followerKey);
    }
        //    查询当前用户是否已关注该实体
    public boolean hasFollowed(int userId, int entityType, int entityId) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
        return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
    }

修改UserController,这里传进来的userId是指被关注者的ID。

    //个人主页,自己访问自己的或者访问别人的
    @RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
    public String getProfilePage(@PathVariable("userId") int userId, Model model){
        User user = userService.findUserById(userId);
        if (user==null){
            throw new RuntimeException("该用户不存在!");
        }
        model.addAttribute("user",user);
        int likeCount = likeService.findUserLikeCount(userId);
        model.addAttribute("likeCount",likeCount);
        //关注数量
        long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
        model.addAttribute("followeeCount",followeeCount);
        //粉丝数量
        long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
        model.addAttribute("followerCount",followerCount);
        //是否关注
        boolean hasFollowed = false;
        if(hostHolder.getUser()!=null){
            hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(),ENTITY_TYPE_USER,userId);
            model.addAttribute("hasFollowed",hasFollowed);
        }
        return "/site/profile";
    }

修改profile.html,展示效果如下:
在这里插入图片描述

在这里插入图片描述

4.3.2 关注列表,粉丝列表

编写业务层FollowService,查询当前用户关注了哪些人,方法findFollowees,传入用户ID,以及分页的偏移量offeset,当前页显示数量limit。首先获得Key,包含用户ID,实体类型为用户,即为3,范围查询使用range方法,但是我们希望页面上是显示最新的,所以使用reverseRange方法。获得集合后进行遍历,查询关注的人以及关注时间。要取关注时间,由于我们存的时候把时间当作分数来存,因此取的时候取分数score即可。查询某用户的粉丝类似逻辑。

    //查询某个用户关注的人
    public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
        String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
        if (targetIds == null) {
            return null;
        }
        List<Map<String, Object>> list = new ArrayList<>();
        for (Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }
        return list;
    }
        //    查询某用户的粉丝
    public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
        String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
        //实现类,是有序集合
        Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);
        if (targetIds == null) {
            return null;
        }
        List<Map<String, Object>> list = new ArrayList<>();
        for (Integer targetId : targetIds) {
            Map<String, Object> map = new HashMap<>();
            User user = userService.findUserById(targetId);
            map.put("user", user);
            Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
            map.put("followTime", new Date(score.longValue()));
            list.add(map);
        }
        return list;
    }

编写FollowController,传入用户ID,根据ID调用业务层方法查询出关注者列表,然后对每个关注者遍历查询是否关注,

    @RequestMapping(path = "/followees/{userId}",method = RequestMethod.GET)
    public String getFollowees(@PathVariable("userId") int userId, Page page, Model model){
        User user = userService.findUserById(userId);
        if (user==null){
            throw new RuntimeException("该用户不存在!");
        }
        model.addAttribute("user",user);
        page.setLimit(5);
        page.setPath("/followees/" + userId);
        page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
        List<Map<String,Object>> userList = followService.findFollowees(userId,page.getOffset(),page.getLimit());
        if (userList!=null){
            for (Map<String, Object> map : userList) {
                User u = (User) map.get("user");
                map.put("hasFollowed",hasFollowed(u.getId()));
            }
        }
        model.addAttribute("users",userList);

        return "/site/followee";
	}
    @RequestMapping(path = "/followers/{userId}",method = RequestMethod.GET)
    public String getFollowers(@PathVariable("userId") int userId, Page page, Model model){
        User user = userService.findUserById(userId);
        if (user==null){
            throw new RuntimeException("该用户不存在!");
        }
        model.addAttribute("user",user);
        page.setLimit(5);
        page.setPath("/followers/" + userId);
        page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER,userId));
        List<Map<String,Object>> userList = followService.findFollowers(userId,page.getOffset(),page.getLimit());
        if (userList!=null){
            for (Map<String, Object> map : userList) {
                User u = (User) map.get("user");
                map.put("hasFollowed",hasFollowed(u.getId()));
            }
        }
        model.addAttribute("users",userList);

        return "/site/follower";

    }
        private boolean hasFollowed(int userId){
        if (hostHolder.getUser()==null){
            return false;
        }
        return followService.hasFollowed(hostHolder.getUser().getId(),ENTITY_TYPE_USER, userId);
    }

修改followee.html和follower.html页面,显示效果如下:
在这里插入图片描述
在这里插入图片描述

4.4 优化登录模块

使用Redis存储验证码

  • 验证码需要频繁的访问与刷新,对性能要求较高。
  • 验证码不需永久保存,通常在很短的时间后就会失效。
  • 分布式部署时,存在Session共享的问题。

使用Redis存储登录凭证

  • 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。

使用Redis缓存用户信息

  • 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高。

4.4.1 使用Redis存储验证码

此时用户还没登录,不能传入用户ID作为唯一凭证拼Key,因此我们用随机字符串给用户,作为登录的唯一凭证,同时拿来拼Key,这个字符串存到Cookie里,设置过期时间短一些。

    private static final String PREFIX_KAPTCHA = "kaptcha";
        //登录验证码
    public static String getKaptchaKey(String owner){
        return PREFIX_KAPTCHA + SPLIT + owner;
    }

修改LoginController,对getCaptcha方法进行重构:原来是存在Session里,现在存到Redis里。前面获取验证码不变,使用UUID给用户,创建Cookie,设置Cookie键和值,设置过期时间为60s,然后拼出Redis的Key,存到Redis里,然后在response中输出验证码图片。

    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response/*, HttpSession session*/){
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);
        // 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 {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image,"png",os);
        } catch (IOException e) {
            logger.error("响应验证码失败" + e.getMessage());
        }

    }

在登录时,对login重构:Session不需要再传入,使用注解@CookieValue获得kaptchaOwner,然后使用redisTemplate查询对应的验证码,对比用户输入的验证码code是否正确,后面逻辑都保持一致。

    @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;
        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_SECOND:DEFAULT_EXPIRED_SECOND;
        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";
        }

    }

4.4.2 使用Redis存储登录凭证

登陆成功后生成登陆凭证,不再保存到MySQL中,而是保存到Redis中。登陆凭证是不删除的,因为后续如果拓展功能,比如看用户什么时候注册的等需要登陆凭证里的信息。

    private static final String PREFIX_TICKET = "ticket";
    //    登录凭证
    public static String getTicketKey(String  ticket){
        return PREFIX_TICKET + SPLIT + ticket;
    }

把LoginTicketMapper废弃,但不用真的删去,而是加上注解@Deprecated。
修改UserService

	//login部分代码
    String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
    redisTemplate.opsForValue().set(redisKey,loginTicket);
    //logout,状态改为1,而不是删掉凭证
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    loginTicket.setStatus(1);
    redisTemplate.opsForValue().set(redisKey,loginTicket);

4.4.3 使用Redis缓存用户信息

缓存的用户信息是会过期的。
首先添加Key的前缀和获取Key的函数。

private static final String PREFIX_USER = "user";
public static String getUserKey(int userId){
    return PREFIX_USER + SPLIT + userId;
}
  1. 查询User时先尝试在缓存中查询,可能可以查询

    private User getCache(int userId){
        String redisKey = RedisKeyUtil.getUserKey(userId);
        return (User) redisTemplate.opsForValue().get(redisKey);
    }
  1. 可能查不到,在数据库中查,并添加缓存
    private User initCache(int userId){
        User user = userMapper.selectById(userId);
        String redisKey = RedisKeyUtil.getUserKey(userId);
        redisTemplate.opsForValue().set(redisKey,user,60*60, TimeUnit.SECONDS);
        return user;
    }
  1. 当数据变化时,数据删掉,数据更新麻烦
    private void clearCache(int userId){
        String redisKey = RedisKeyUtil.getUserKey(userId);
        redisTemplate.delete(redisKey);
    }

修改查询User的函数

    public User findUserById(int id){
        // return userMapper.selectById(id);
        User user = getCache(id);
        if (user==null){
            user = initCache(id);
        }
        return user;
    }

修改User信息的地方,都要把Redis中User的缓存清理掉

    public int activation(int userId, String code){
        User user = userMapper.selectById(userId);
        if(user.getStatus()==1){
            return ACTIVATION_REPEAT;
        }
        else if(user.getActivationCode().equals(code)){
            userMapper.updateStatus(userId,1);
            //数据更新,清除缓存
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        }
        else{
            return ACTIVATION_FAIL;
        }
    }
    public int updateHeader(int userId, String headerUrl){
        // return userMapper.updateHeader(userId, headerUrl);
        int rows = userMapper.updateHeader(userId, headerUrl);
        clearCache(userId);
        return rows;
    }
    public int updatePassword(int userId, String password, String salt){
        // return userMapper.updatePassword(userId,CommunityUtil.md5(password + salt));
        int rows = userMapper.updatePassword(userId, CommunityUtil.md5(password + salt));
        clearCache(userId);
        return rows;
    }

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Git系列:git merge 使用技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Redis基础面试知识点(1)

相比于C字符串&#xff0c;SDS的优势&#xff1a; O(1)获取字符串的长度不会缓冲区溢出减少修改字符串时所需的内存重新分配的次数&#xff08;空间预分配、惰性空间释放&#xff09;二进制API安全&#xff08;通过len获取长度&#xff09;兼容部分C字符串函数 Redis hash策略…

SpringAMQP Work Queue 工作队列

消息模型: 代码模拟: 相较于之前的基础队列&#xff0c;该队列新增了消费者 不再是一个&#xff0c;所以我们通过代码模拟出两个consumer消费者。在原来的消费者类里写两个方法 其中消费者1效率高 消费者2效率低 RabbitListener(queues "simple.queue")public voi…

机器学习算法应用——CART决策树

CART决策树&#xff08;4-2&#xff09; CART&#xff08;Classification and Regression Trees&#xff09;决策树是一种常用的机器学习算法&#xff0c;它既可以用于分类问题&#xff0c;也可以用于回归问题。CART决策树的主要原理是通过递归地将数据集划分为两个子集来构建决…

【nodejs 命令行交互神器 - inquirer.js】

需求 大家在开发时&#xff0c;有时需要从命令行读取用户的输入&#xff0c;或者让用户选择。在nodejs中&#xff0c;这个怎么实现? 原生实现 ❌ process.stdin.setEncoding(utf8);process.stdin.on(readable, () > {let chunk;// 使用循环确保我们读取所有的可用输入wh…

【C++算法】队列相关经典算法题

1. N叉树的层序遍历 首先我们遇到这个题目&#xff0c;没有任何思路&#xff0c;我们就可以来模拟一下层序的流程&#xff0c;首先我们肯定是访问根节点1&#xff0c;访问之后呢就是访问下一层的最左节点3&#xff0c;此时第一层的节点1已经访问过了就可以不要了&#xff0c;然…

词令蚂蚁新村今日答案:微信小程序怎么查看蚂蚁新村今天问题的正确答案?

微信小程序怎么查看蚂蚁新村今天问题的正确答案&#xff1f; 1、打开微信&#xff0c;点击搜索框&#xff1b; 2、打开搜索页面&#xff0c;选择小程序搜索&#xff1b; 3、在搜索框&#xff0c;输入词令搜索点击进入词令微信小程序&#xff1b; 4、打开词令微信小程序关键词口…

每日10亿数据的日志分析系统OOM

背景 一个每日10亿数据的日志清洗系统&#xff0c;主要工作就是从消息队列中消费各种各样的日志&#xff0c;然后对日志进行清洗&#xff0c;例如&#xff1a;用户敏感信息(姓名、手机号、身份证)进行脱敏处理,然后把清理完的数据交付给其他系统使用。 我们项目中&#xff0c;…

设计模式2——原则篇:依赖倒转原则、单一职责原则、合成|聚合复用原则、开放-封闭原则、迪米特法则、里氏代换原则

设计模式2——设计原则篇 目录 一、依赖倒转原则 二、单一职责原则&#xff08;SRP&#xff09; 三、合成|聚合复用原则&#xff08;CARP&#xff09; 四、开放-封闭原则 五、迪米特法则&#xff08;LoD&#xff09; 六、里氏代换原则 七、接口隔离原则 八、总结 一、依赖…

《2024年AI安全报告》:AIML工具使用量飙升594.82%

人工智能&#xff08;AI&#xff09;不仅仅是一种开拓性的创新技术&#xff0c;甚至已经成为一种常态&#xff0c;企业正在工程、IT营销、财务、客户服务等领域迅速采用AI和机器学习&#xff08;ML&#xff09;工具。但与此同时&#xff0c;他们必须平衡AI工具带来的诸多风险&a…

JWT深入浅出

文章目录 JWT深入浅出1.JWT是什么2.为什么选JWT2.1 传统Session认证2.2 JWT认证 3.JWT怎么用4. jwt绝对安全吗&#xff1f; JWT深入浅出 1.JWT是什么 JWT&#xff08;JSON Web Token&#xff09;是一种用于在网络应用间传递信息的开放标准&#xff0c;通常用于身份认证和非敏…

24寸2K显示器 - HKC G24H2

&#x1f525;&#x1f5a5;️ 嘿&#xff0c;大家好&#xff01;今天&#xff0c;我要给大家介绍一款超棒的显示器——HKCG24H2&#xff01;这款显示器可是个全能选手&#xff0c;无论你是工作狂人还是游戏迷&#xff0c;它都能满足你的需求&#xff01; &#x1f60e;&#x…

传输层之 UDP 协议

UDP协议端格式 教科书上的&#xff1a; 16位UDP长度&#xff0c;表示整个数据报&#xff08;UDP首部UDP数据&#xff09;的最大长度&#xff0c;描述了这个数据报多长&#xff1b; 实际上的&#xff1a; UDP 会把载荷数据&#xff0c;就是通过 UDP Socket&#xff0c;即 sen…

PyQt5批量生成Checkbox及批量检查Checkbox的勾选状态

批量生成Checkbox并添加到TableWidget中 for i in range(10):checkbox_i QCheckBox(fCheckbox_{i}) # 生成Checkbox并命名为Checkbox_iself.ui_1.tableWidget_1.setCellWidget(i,1,checkbox_i) 批量检查勾选状态 # 批量生成Checkbox并存入列表 list_Checkbox_1 [] for …

vue3专栏项目 -- 三、使用vue-router 和 vuex(下)

一、添加columnDetail 页面 首页有专栏列表&#xff08;ColumnList组件&#xff09;&#xff0c;专栏列表中有很多专栏&#xff0c;然后点击某个专栏就进入专栏详情页&#xff08;ColumnDetail组件&#xff09;&#xff0c;专栏详情页中有很多文章&#xff0c;点击某个文章就进…

uni-segmented-control插件使用

dcloud插件市场 前端/uniapp 1.HBuildX打开目标项目 2.进入dcloud插件市场下载目标插件 3.看到如下提示(已经可以在目标项目中使用插件啦) 4.项目正式使用

物联网平台之单体架构

介绍本文主要介绍平台的单体架构&#xff0c;包括各个组件之间的数据流描述以及所做的一些架构选择。在单体架构模式下&#xff0c;所有 ThingsKit 组件都在单个 Java 虚拟机 (JVM) 中启动&#xff0c;并共享相同的操作系统资源。由于 ThingsKit 是用 Java 编写的&#xff0c;因…

springcloud alibaba微服务框架涉及的技术

一、微服务架构中核心模块及其使用技术总览 二、各模块详细说明 1、注册中心 该模块主要功能为 自动提供服务的注册与发现&#xff0c;集中式管理服务&#xff0c;让 服务调用端发现服务&#xff0c;让服务提供端注册服务&#xff0c;倘若没有注册中心&#xff0c;那客户端就…

JUC下的BlockingQueue详解

BlockingQueue是Java并发包(java.util.concurrent)中提供的一个接口&#xff0c;它扩展了Queue接口&#xff0c;增加了阻塞功能。这意味着当队列满时尝试入队操作&#xff0c;或者队列空时尝试出队操作&#xff0c;线程会进入等待状态&#xff0c;直到队列状态允许操作继续。这…

记录使用极空间NAS通过Docker部署小皮面板(PhpStydy)运行 八图片当面付支付宝接口 PHP项目的遭遇

事件的起因还得从我用八图片的图片加密支付跳转功能&#xff0c;实现打赏金额发案例源码下载链接挣个烟钱的事。八图片的支付接口是PHP web项目的。正好我有个极空间的NAS&#xff0c;搭建到NAS上省去了买主机的费用。 导读 八图片是什么&#xff1f;极空间NAS 部署 PHP网站安装…