仿牛客社区

发布于:2022-11-28 ⋅ 阅读:(263) ⋅ 点赞:(0)

第1章 初识SpringBoot ,开发社区首页

1.1 课程介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2 搭建开发环境

1.创建community项目

在这里插入图片描述添加基本的依赖
pom.xml

<!--启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web启动依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

编写配置文件
application.yml

server:
  port: 8080
  servlet:
    context-path: /community

启动项目,输入http://localhost:8080/community测试项目启动

2.导入数据库

评论表

CREATE TABLE `comment` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `user_id` INT(11) DEFAULT NULL,
  `entity_type` INT(11) DEFAULT NULL,
  `entity_id` INT(11) DEFAULT NULL,
  `target_id` INT(11) DEFAULT NULL,
  `content` TEXT,
  `status` INT(11) DEFAULT NULL,
  `create_time` TIMESTAMP NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */,
  KEY `index_entity_id` (`entity_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

帖子表

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;

登录凭证表

CREATE TABLE `login_ticket` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `user_id` INT(11) NOT NULL,
  `ticket` VARCHAR(45) NOT NULL,
  `status` INT(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',
  `expired` TIMESTAMP NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_ticket` (`ticket`(20))
) ENGINE=INNODB DEFAULT CHARSET=utf8;

消息表

CREATE TABLE `message` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `from_id` INT(11) DEFAULT NULL,
  `to_id` INT(11) DEFAULT NULL,
  `conversation_id` VARCHAR(45) NOT NULL,
  `content` TEXT,
  `status` INT(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',
  `create_time` TIMESTAMP NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_from_id` (`from_id`),
  KEY `index_to_id` (`to_id`),
  KEY `index_conversation_id` (`conversation_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

用户表

CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) DEFAULT NULL,
  `password` VARCHAR(50) DEFAULT NULL,
  `salt` VARCHAR(50) DEFAULT NULL,
  `email` VARCHAR(100) DEFAULT NULL,
  `type` INT(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;',
  `status` INT(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;',
  `activation_code` VARCHAR(100) DEFAULT NULL,
  `header_url` VARCHAR(200) DEFAULT NULL,
  `create_time` TIMESTAMP NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_username` (`username`(20)),
  KEY `index_email` (`email`(20))
) ENGINE=INNODB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;

3.使用Mybatis

导入依赖
pom.xml

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>

编写配置文件
application.yml

spring:
  # 数据源配置
  datasource:
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/community?serverTimezone=UTC
    type: com.zaxxer.hikari.HikariDataSource
    hikari: # 连接池配置
      maximum-pool-size: 15
      minimum-idle: 5
      idle-timeout: 30000

# mybatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.comunity.entity
  configuration:
    use-generated-keys: true # 自动生成主键
    map-underscore-to-camel-case: true # 驼峰命名映射

编写实体类

/**
 * 用户表
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
    private String salt; //用来加密
    private String email;
    private int type;
    private int status;
    private String activationCode; //激活码
    private String headerUrl;
    private Date createTime;

}

UserMapper

@Mapper
public interface UserMapper {
    // 通过id查询用户
    User selectById(int id);
    // 通过username查询用户
    User selectByName(String username);
    // 通过邮箱查询用户
    User selectByEmail(String email);
    // 新增用户
    int insertUser(User user);
    // 修改用户状态
    int updateStatus(int id,int status);
    // 修改用户头像
    int updateHeader(int id,String headerUrl);
    // 修改用户密码
    int updatePassword(int id,String password);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.UserMapper">

    <sql id="selectFields">
        id,username,password,salt,email,type,status,activation_code,header_url,create_time
    </sql>

    <sql id="insertFields">
        username,password,salt,email,type,status,activation_code,header_url,create_time
    </sql>

    <select id="selectById" resultType="User">
        select
        <include refid="selectFields"></include>
        from user
        where id = #{id}
    </select>

    <select id="selectByName" resultType="User">
        select
        <include refid="selectFields"></include>
        from user
        where username = #{username}
    </select>

    <select id="selectByEmail" resultType="User">
        select
        <include refid="selectFields"></include>
        from user
        where email = #{email}
    </select>

    <!--mybatis自动生成id,通过keyProperty将id填入user 不然为空-->
    <insert id="insertUser" parameterType="User" keyProperty="id">
        insert into user(<include refid="insertFields"></include>)
        values(#{username},#{password},#{salt},#{email},#{type},#{status},#{activationCode},#{headerUrl},#{createTime})
    </insert>

    <update id="updateStatus">
        update user
        set status=#{status}
        where id = #{id}
    </update>

    <update id="updateHeader">
        update user
        set header_url=#{headerUrl}
        where id = #{id}
    </update>

    <update id="updatePassword">
        update user
        set password=#{password}
        where id = #{id}
    </update>

</mapper>

编写测试类用来测试sql

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MapperTests {

    @Autowired
    private UserMapper userMapper;
}

查询

	@Test
    public void TestSelectUser() {
        User user = userMapper.selectById(101);
        System.out.println(user);
        user= userMapper.selectByName("张三");
        System.out.println(user);
        user = userMapper.selectByEmail("1345313@sina.com");
        System.out.println(user);
    }

在这里插入图片描述插入

	@Test
    public void  TestInsertUser(){
        User user = new User();
        user.setUsername("李四");
        user.setPassword("123456");
        user.setSalt("abc");
        user.setEmail("lisi@qq.com");
        user.setHeaderUrl("");
        user.setCreateTime(new Date());

        int row = userMapper.insertUser(user);
        System.out.println(row);
        System.out.println(user.getId());
    }

在这里插入图片描述修改

	@Test
    public void TestUpdateUser(){
        int row = userMapper.updateStatus(123, 1);
        System.out.println(row);

        row = userMapper.updateHeader(123, "123.com");
        System.out.println(row);

        row = userMapper.updatePassword(123, "23123");
        System.out.println(row);
    }

在这里插入图片描述

1.3 开发社区首页

在这里插入图片描述

1. 准备环境

导入静态资源
在这里插入图片描述

引入thymealeaf
pom.xml

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

关闭thymealeaf模板引擎
application.yml

  # 关闭缓存
  thymeleaf:
    cache: false

2. 代码开发

实体类

DiscussPost

/**
 * 帖子表
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DiscussPost {

    private Integer id;
    private Integer userId;
    private String title;
    private String content;
    // 类型 0:普通 1:置顶
    private Integer type;
    // 状态 0:正常 1:精华 2:拉黑
    private Integer status;
    private Date createTime;
    private Integer commentCount; // 评论数量
    private double score;
}

数据访问层

DiscussPostMapper

@Mapper
public interface DiscussPostMapper {

    // 根据用户id查询帖子列表,动态sql(也可以不使用userId)
    List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
    // 查询数据行数  动态拼接条件且只有一个参数时 需要@Param
    int selectDiscussPostRows(@Param("userId") int userId);

}

DiscussPostMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.DiscussPostMapper">

    <sql id="selectFields">
        id,user_id,title,content,type,status,create_time,comment_count,score
    </sql>

    <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>
        order by type desc,create_time desc
        limit #{offset},#{limit}
    </select>

    <select id="selectDiscussPostRows" resultType="int">
        select count(id)
        from discuss_post
        where status!=2
        <if test="userId!=0">
            and user_id=#{userId}
        </if>
    </select>

</mapper>

测试sql

/**
     * DiscussPostMapper
     */
    @Test
    public void testSelectPosts(){
        List<DiscussPost> list = discussPostMapper.selectDiscussPosts(0, 0, 10);
        for (DiscussPost discussPost : list) {
            System.out.println(discussPost);
        }

        int rows = discussPostMapper.selectDiscussPostRows(0);
        System.out.println(rows);

    }

在这里插入图片描述

数据业务层

DiscussPostService

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    /**
     * 查询帖子列表
     * @param userId
     * @param offset
     * @param limit
     * @return
     */
    public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit){
        return discussPostMapper.selectDiscussPosts(userId, offset, limit);
    }

    /**
     * 获取帖子行数
     * @param userId
     * @return
     */
    public int findDiscussPostRows(int userId){
        return discussPostMapper.selectDiscussPostRows(userId);
    }
}

UserService

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据id查询用户信息
     * @param id
     * @return
     */
    public User findUserById(int id){
        return userMapper.selectById(id);
    }
}

视图层

HomeController

@Controller
public class HomeController {

    @Autowired
    private DiscussPostService discussPostService;
    @Autowired
    private UserService userService;

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String GetIndexPage(Model model){
        List<DiscussPost> list = discussPostService.findDiscussPosts(0, 0, 10);
        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);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts",discussPosts);
        return "/index";
    }
}

index.html
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
启动测试

输入http://localhost:8080/community/index
在这里插入图片描述
实现分页

Page

/**
 * 封装分页相关信息
 */
@Data
public class Page {

    // 当前页码
    private int current = 1;
    // 显示上限
    private int limit = 10;
    // 数据总数 (用于计算总页数)
    private int rows;
    // 查询路径 (用于复用分页链接)
    private String path;

    public void setCurrent(int current) {
        if (current >= 1) {
            this.current = current;
        }
    }

    public void setLimit(int limit) {
        if (limit >= 1 && limit <= 100) {
            this.limit = limit;
        }
    }

    public void setRows(int rows) {
        if (rows >= 0) {
            this.rows = rows;
        }
    }

    /**
     * 获取当前页的起始页
     *
     * @return
     */
    public int getOffset() {
        // current * limit -limit
        return (current - 1) * limit;
    }

    /**
     * 获取总页数
     *
     * @return
     */
    public int getTotal() {
        // rows / limit [+1]
        if (rows % limit == 0) {
            return rows / limit;
        } else {
            return rows / limit + 1;
        }
    }

    /**
     * 获取起始页码
     *
     * @return
     */
    public int getFrom() {
        int from = current - 2;
        return from < 1 ? 1 : from;
    }

    /**
     * 获取结束页码
     * @return
     */
    public int getTo() {
        int to = current + 2;
        int total = getTotal();
        return to > total ? total : to;
    }
}

优化Controller代码

HomeController

@RequestMapping(path = "/index", method = RequestMethod.GET)
    public String GetIndexPage(Model model, Page page){
        // 方法调用前,SpringMVC会自动实力胡model和page,并将Page注入Model
        // 所以,在thymealeaf中可以直接访问Page对象中的数据
        page.setRows(discussPostService.findDiscussPostRows(0));
        page.setPath("/index");

        // 获取索引帖子信息
        List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
        // 向页面返回的集合
        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);

                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts",discussPosts);
        return "/index";
    }

将页面静态效果修改为动态效果

<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}" th:fragment="pagination">
    <ul class="pagination justify-content-center" >
        <li class="page-item">
            <a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>
        </li>
        <li th:class="|page-item ${page.current==1?'disabled':''}|">
            <a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a>
        </li>
        <li th:class="|page-item ${i==page.current?'active':''}|"
            th:each="i:${#numbers.sequence(page.getFrom(),page.getTo())}">
            <a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>
        </li>
        <li th:class="|page-item ${page.current==page.getTotal()?'disabled':''}|">
            <a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a>
        </li>
        <li class="page-item">
            <a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a>
        </li>
    </ul>
</nav>

在这里插入图片描述

第2章 SpringBoot实践,开发社区登录模块

1、发送邮件

在这里插入图片描述

启用SMTP服务
在这里插入图片描述
jar包

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

编写配置文件
application.yml
在这里插入图片描述
这里邮件发送我用的是qq邮箱

工具类MailSender

/**
 * 邮件发送
 */
@Component
public class MailClient {
    // 声明log
    private static  final Logger logger= LoggerFactory.getLogger(MailClient.class);

    @Autowired
    private JavaMailSender mailSender;

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

    /**
     * 发送邮件
     * @param to 收件人
     * @param subject 标题
     * @param content 内容
     */
    public void sendMail(String to,String subject,String content){

        try {
            // 创建message对象
            MimeMessage message =mailSender.createMimeMessage();
            MimeMessageHelper helper =new MimeMessageHelper(message);
            // 设置邮件信息
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content,true); // 可转换html标签
            // 发送邮件
            mailSender.send(helper.getMimeMessage());
        } catch (MessagingException e) {
            logger.error("发送邮件失败"+e.getMessage());
        }
    }
}

测试

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {

    @Autowired
    private MailClient mailClient;

    @Test
    public void testTextMail(){
        mailClient.sendMail("@qq.com","test","welcome");
    }
}

接收到的邮件效果

在这里插入图片描述

利用Html模板发送邮件

templates/mail/activation.html

引入thymeleaf,传入一个username参数
在这里插入图片描述
发送模板邮件测试

	@Autowired
    private TemplateEngine templateEngine;

	@Test
    public void testHtmlMail(){
        Context context=new Context();
        context.setVariable("username", "sunday");

        String content=templateEngine.process("/mail/activation",context);
        System.out.println(content);

        mailClient.sendMail("@qq.com","html",content);

    }

控制台输出
在这里插入图片描述
邮箱内容
在这里插入图片描述

2、开发注册功能

在这里插入图片描述

2.1 访问注册页面

编写LoginController来获取注册页面
LoginController

@Controller
public class LoginController {

    /**
     * 获取注册页面
     * @return
     */
    @RequestMapping(value = "/register",method = RequestMethod.GET)
    public String getRegisterPage(){
        return "/site/register";
    }
}

使用thymeleaf对页面进行修改

静态资源路径
在这里插入图片描述
修改主页的头部信息
在这里插入图片描述
效果

点击注册进入注册页面
在这里插入图片描述

2.2 提交注册数据

准备工作

  1. 导入jar包
<!--commons-lang-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
  1. 配置网站域名
# community配置
community:
  path:
    domain: http://localhost:8080
  1. 编写工具类

CommunityUtil
用来生成随机字符串

public class CommunityUtil {
    /**
     * 生成随机字符串
     * @return
     */
    public static String generateUUID(){
        return UUID.randomUUID().toString().replaceAll("-","");
    }

    /**
     * MD5加密
     * @param key
     * @return
     */
    public static String md5(String key){
        // 空格 null 空串都为空
        if (StringUtils.isBlank(key)){
            return null;
        }
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

代码开发

UserService

/**
     * 用户注册
     * @param user
     * @return
     */
    public Map<String, Object> register(User user) {
        Map<String, Object> map = new HashMap<>();
        // 1. 空值处理
        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;
        }

        // 2.1 验证账号
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在!");
            return map;
        }
        // 2.2 验证邮箱
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册!");
            return map;
        }

        // 3. 注册用户
        // 获取随机字符串
        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);

        // 4.发送激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        // http://localhost:8080/community/activation/101/code
        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;
    }

LoginController

/**
     * 用户注册
     * @param model
     * @param user
     * @return
     */
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(Model model, User user) {
        Map<String, Object> map = userService.register(user);
        // 判断map
        if (map == null || map.isEmpty()) {
            model.addAttribute("msg","注册成功,我们已经想您的邮箱发送了一封激活邮件,请尽快激活!");
            model.addAttribute("target","/index"); // 跳转的目标页面
            return "/site/operate-result";
        }
        model.addAttribute("usernameMsg",map.get("usernameMsg"));
        model.addAttribute("passwordMsg",map.get("passwordMsg"));
        model.addAttribute("emailMsg",map.get("emailMsg"));

        return "/site/register";

    }

页面处理

发送验证码邮件页面 activation.html
在这里插入图片描述注册成功中转页面 operate-result.html
在这里插入图片描述
注册页面 register.html
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
测试效果

测试是否可以注册已经注册过的账号或者邮箱
在这里插入图片描述
在这里插入图片描述

注册成功页面跳转
在这里插入图片描述
成功发送邮件
在这里插入图片描述

2.3 激活注册账号

准备工作

编写常量类,来记录账号的状态,

/**
 * 常量
 */
public interface CommunityConstant {

    /**
     * 激活成功
     */
    int ACTIVATION_SUCCESS = 0;

    /**
     * 激活成功
     */
    int ACTIVATION_REPEAT = 1;

    /**
     * 激活失败
     */
    int ACTIVATION_FAILURE = 2;

}

使用常量接口时,需要先实现接口

代码开发

UserService

/**
     * 激活账号
     *
     * @param userId
     * @param code
     * @return
     */
    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);
            return ACTIVATION_SUCCESS;
        } else {
            return ACTIVATION_FAILURE;
        }
    }

LoginController

		/**
     * 获取登录页面
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String getLoginPage(){
        return "/site/login";
    }
		/**
     * 激活账号
     *
     * @param model
     * @param userId
     * @param code
     * @return
     */
    // http://localhost:8080/community/activation/101/code
    @RequestMapping(value = "/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";
    }

修改登录页面login.html
在这里插入图片描述在这里插入图片描述

在主页index.html中需改登陆页面的路径
在这里插入图片描述
效果

点击登录,可以跳转到登录页面
在这里插入图片描述
点击邮件中的此链接即可激活账号
在这里插入图片描述

3、生成验证码

在这里插入图片描述
准备工作

  1. 导入jar包
<!--kaptcha-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
  1. 编写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","0123456789ASCDEFGHIJKLMNOPQRSTUVWXYZ");
        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;
    }
}
  1. LoginController

编写方法为前端页面提供图片验证码

/**
     * 获取图片验证码
     * @param response
     * @param session
     */
    @RequestMapping(value = "/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);

        // 将图片输出给浏览器
        response.setContentType("image/png"); // 响应类型
        try {
            ServletOutputStream os = response.getOutputStream();
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("相应验证码失败:" + e.getMessage());
        }

    }

测试

输入localhost:8080/community/kaptcha可以产看已经生成的验证码
在这里插入图片描述
4. 修改前端页面

login.html

在这里插入图片描述
刷新验证码

在这里插入图片描述

4、登录、退出功能

在这里插入图片描述

4.1 准备工作

实体类

LoginTicket

/**
 * 登陆凭证表
 */
@Data
public class LoginTicket {

    // 主键
    private int id;
    // 用户id
    private int userId;
    // 凭证
    private String ticket;
    // 状态 0:正常 1:过期
    private int status;
    // 过期时间
    private Date expired;
}

4.2 登录

LoginTicketMapper

编写所需功能的Mapper接口

@Mapper
public interface LoginTicketMapper {

    // 插入一个凭证
    @Insert({
            "insert into login_ticket(user_id,ticket,status,expired) ",
            "values(#{userId},#{ticket},#{status},#{expired}) "
    })
    @Options(useGeneratedKeys = true,keyProperty = "id") // id自增,id为主键
    int insertLoginTicket(LoginTicket loginTicket);

    // 根据ticket查询
    @Select({
            "select id,user_id,ticket,status,expired",
            "from login_ticket where ticket=#{ticket}"
    })
    LoginTicket selectByTicket(String ticket);

    // 修改状态(相当于删除)
    @Update({
            "update login_ticket set status=#{status} ",
            "where ticket=#{ticket}"
    })
    int updateStatus(String ticket,int status);
}

UserService

由于登录跟用户相关,所以直接写到UserService

/**
     * 用户登录
     *
     * @param username
     * @param password
     * @param expiredSeconds
     * @return
     */
    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 (!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);

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

    }

注意:若过期事件为int类型,数据会造成溢出,数据库中的过期时间会与结果有所差别。

LoginController

/**
     * 用户登录
     * @param username
     * @param password
     * @param code
     * @param rememberMe
     * @param model
     * @param session
     * @param response
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(String username, String password, String code, boolean rememberMe,
                        Model model, HttpSession session, HttpServletResponse response) {
        // 检查验证码
        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_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";
        }
    }

前端页面修改

login.html在这里插入图片描述
在这里插入图片描述

4.3 退出

UserService

/**
     * 退出
     * @param ticket
     */
    public void logout(String ticket){
        loginTicketMapper.updateStatus(ticket,1);
    }

LoginController

/**
     * 退出
     * @param ticket
     * @return
     */
    @RequestMapping(value = "/logout",method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket){
        userService.logout(ticket);
        return "redirect:/login";
    }

5、显示登录信息

在这里插入图片描述
执行流程

在这里插入图片描述
代码实现

5.1 在请求开始时查询登录用户

UserService

/**
     * 根据ticket查询登录凭证信息
     * @param ticket
     * @return
     */
    public LoginTicket findLoginTicket(String ticket){
        return loginTicketMapper.selectByTicket(ticket);
    }

LoginTicketInterceptor

// controller之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.从cookie中获取ticket
        String ticket = CookieUtil.getValue(request, "ticket");
        // 2.查询凭证
        if (ticket != null) {
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            // 凭证是否有效
            if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证id查询到用户
                User user = userService.findUserById(loginTicket.getUserId());
                // 在本次请求中持有用户
                hostHolder.setUser(user);
            }
        }
        return true;
    }

5.2 在本次请求中持有用户数据

CookieUtil

public class CookieUtil {
    public static String getValue(HttpServletRequest request, String name) {

        if (request == null || name == null) {
            throw new IllegalArgumentException("参数为空");
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

5.3 在模板视图上显示用户数据

LoginInterceptor

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    //得到当前线程持有的user
    User user = hostHolder.getUser();
    System.out.println(user);
    if (user != null && modelAndView != null) {
        modelAndView.addObject("loginUser", user);
    }
}

index.html

5.4 在请求结束时清理用户数据

UserService

//修改凭证
public void  logout(String ticket){
    loginTicketMapper.updateStatus(ticket,1);
}

LoginInterceptor

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    //清理数据
    hostHolder.clear();
}

5.5 注册bean

WebMvcConfig

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");//拦截所有请求
    }
}

6、账号设置

在这里插入图片描述

6.1 访问账设置页面

创建UserController来获取setting页面

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping(value = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
        return "/site/setting";
    }
}

修改前端页面
index.html
在这里插入图片描述
setting.html
在这里插入图片描述
在这里插入图片描述
效果

点击头像上的账号设置即可跳转到setting.html页面
在这里插入图片描述

6.2 上传头像

UserController

/**
     * 上传头像
     *
     * @param headerImage
     * @param model
     * @return
     */
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "你还没有选择图片");
            // 回到设置页面
            return "/site/setting";
        }
        // 得到原始文件名
        String filename = headerImage.getOriginalFilename();
        // 获取文件名后缀
        String suffix = filename.substring(filename.lastIndexOf("."));
        // 对后缀名进行判断
        if (StringUtils.isBlank(suffix)) {
            model.addAttribute("error", "文件格式不正确");
            return "/site/setting";
        }

        // 生成随机文件名
        filename = CommunityUtil.generateUUID() + suffix;

        // 文件存储位置
        File dest = new File(uploadPath);
        // 如果文件目录不存在
        if (!dest.exists()) {
            // 自动生成文件夹
            dest.mkdirs();
        }
        try {
            // 存储文件
            headerImage.transferTo(new File(dest +"/"+ filename));
        } catch (IOException e) {
            logger.error("上传文件失败" + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!", e);
        }
        // 更新当前用户头像路径(web访问路径)
        // http://localhost:8080/community/user/header/xxx.png
        User user = hostHolder.getUser();
        String headerUrl = domain + contextPath + "/user/header/" + filename;
        userService.updateHeader(user.getId(), headerUrl);

        //重定向回首页
        return "redirect:/index";

    }

需要注意的是:截取后缀名时,一定要注意截取的位置是否准确,jpg还是.jpg

前端页面修改

setting.html
在这里插入图片描述

6.3 获取头像

UserController

/**
     * 获取头像
     *
     * @param fileName
     * @param response
     */
    @RequestMapping(value = "/header/{fileName}", method = RequestMethod.GET)
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        // 服务器存放的路径
        fileName = uploadPath + "/" + fileName;
        // 获取文件后缀
        String suffix = fileName.substring(fileName.lastIndexOf(".")+1);
        // 响应图片
        response.setContentType("image/" + suffix);

        try (
                FileInputStream fis = new FileInputStream(fileName);
                OutputStream os = response.getOutputStream();
        ) {
            // 建立缓冲区
            byte[] buffer = new byte[1024];
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取头像失败" + e.getMessage());
        }
     }

6.4 修改密码

UserService

我们需要获取到原来的密码、新密码、确认密码来进行修改密码以及判断。

//修改密码
public Map<String, Object> updatePassword(User user, String oldPassword, String newPassword, String checkPassword) {
    Map<String, Object> map = new HashMap<>();
    if (StringUtils.isBlank(oldPassword)) {
        map.put("oldPasswordMsg", "原始密码不能为空");
        return map;
    }
    if (StringUtils.isBlank(newPassword)) {
        map.put("newPasswordMsg", "新的密码不能为空");
        return map;
    }
    if (StringUtils.isBlank(checkPassword)) {
        map.put("checkPasswordMsg", "两次密码不能为空");
        return map;
    }
    if (!newPassword.equals(checkPassword)) {
        map.put("checkPasswordMsg", "两次密码不一致");
        return map;
    }
    //获取旧密码
    oldPassword = CommunityUtil.md5(newPassword + user.getSalt());
    if (!oldPassword.equals(user.getPassword())) {
        map.put("oldPassword", "原始密码错误");
        return map;
    }
    System.out.println("旧密码---"+oldPassword);
    //获取新密码
    newPassword=CommunityUtil.md5(newPassword+user.getSalt());
    System.out.println("新密码---"+newPassword);
    //修改密码
    userMapper.updatePassword(user.getId(), newPassword);

    return map;
}

UserController

//修改密码
@LoginRequired
@RequestMapping(value = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(Model model, String oldPassword, String newPassword, String checkPassword) {
    //获取user对象
    User user = hostHolder.getUser();
    //System.out.println("获得到的user:"+user);//d82c0e5494e88f041cab523fd31881b6
    //System.out.println(newPassword);//123
    Map<String, Object> map = userService.updatePassword(user, oldPassword, newPassword, checkPassword);
    //判空
    if (map == null || map.isEmpty()) {
        return "redirect:/index";
    } else {
        System.out.println("进入报错区");
        model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg"));
        model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
        model.addAttribute("checkPasswordMsg",map.get("checkPasswordMsg"));
        return "/site/setting";
    }
}

修改前端页面

setting.html
在这里插入图片描述

7、检查登录状态

在这里插入图片描述
编写自定义注解LoginRequired

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}

在需要拦截的方法上添加注解
在这里插入图片描述

编写拦截器

拦截只有loginRequired注解的请求

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Autowired
    private HostHolder hostHolder;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果拦截的是方法
        if (handler instanceof HandlerMethod){
            //转型
            HandlerMethod handlerMethod= (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            if (loginRequired!=null&&hostHolder.getUser()==null){
                response.sendRedirect(request.getContextPath()+"/login");
                return false;
            }
        }
        return true;
    }
}

在WebMvcConfig中注册拦截器
在这里插入图片描述

第3章 核心功能

1、敏感词过滤

在这里插入图片描述
前缀树
在这里插入图片描述
构建一颗前缀树

/**
     * 1.定义前缀树
     */
    private class TrieNode {
        // 标志位
        private boolean isKeywordEnd = false;

        // 子节点 map  key: 下级字符 value:下级节点
        Map<Character, TrieNode> subNodes = new HashMap<>();

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

        // 获取子节点
        public TrieNode getSubNodes(Character c) {
            return subNodes.get(c);
        }

        // 添加子节点的方法
        public void addSubNodes(Character c, TrieNode node) {
            subNodes.put(c, node);
        }
    }

初始化前缀树

/**
     * 2.初始化前缀树
     */
    @PostConstruct
    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) {
            logger.error("加载敏感过滤器文件失败:" + e.getMessage());
        }
    }

检索敏感词并替换

/**
     * 3.检索敏感词
     *
     * @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处于root节点,将符号计入结果,指针2向下走一步
                if (tempNode == rootNode) {
                    sb.append(c);
                    begin++;
                }
                // 无论符号在开头或中间,指针3都向下走一步
                position++;
                continue;// 进入下一轮循环

            } else {
                // 检查下级节点
                tempNode = tempNode.getSubNodes(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++;
                }
            }
        }
        // 将最后一批字符计入结果
        sb.append(text.substring(begin));
        return sb.toString();
    }

整体代码

SensitiveFilter

@Component
public class SensitiveFilter {

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

    // 定义替换符
    private static final String REPLACEMENT = "***";

    // 定义根节点
    private TrieNode rootNode = new TrieNode();

    /**
     * 2.初始化前缀树
     */
    @PostConstruct
    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) {
            logger.error("加载敏感过滤器文件失败:" + e.getMessage());
        }
    }

    /**
     * 将敏感词添加到前缀树
     *
     * @param keyword
     */
    private void addKeyword(String keyword) {
        // 根节点
        TrieNode tempNode = rootNode;

        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);
            // 获取子节点
            TrieNode subNode = tempNode.getSubNodes(c);
            // 判断子节点是否存在
            if (subNode == null) {
                // 初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNodes(c, subNode);
            }
            // 如果已存在 指向子节点,进入下一轮循环
            tempNode = subNode;
            // 设置结束标识
            if (i == keyword.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }

    /**
     * 3.检索敏感词
     *
     * @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处于root节点,将符号计入结果,指针2向下走一步
                if (tempNode == rootNode) {
                    sb.append(c);
                    begin++;
                }
                // 无论符号在开头或中间,指针3都向下走一步
                position++;
                continue;// 进入下一轮循环

            } else {
                // 检查下级节点
                tempNode = tempNode.getSubNodes(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++;
                }
            }
        }
        // 将最后一批字符计入结果
        sb.append(text.substring(begin));
        return sb.toString();
    }

    /**
     * 判断是否为符号
     *
     * @param c
     * @return
     */
    private boolean isSymbol(char c) {
        // 0x2E80 ~ 0x9FFF 是东亚文字范围
        return !CharUtils.isAsciiPrintable(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    /**
     * 1.定义前缀树
     */
    private class TrieNode {
        // 标志位
        private boolean isKeywordEnd = false;

        // 子节点 map  key: 下级字符 value:下级节点
        Map<Character, TrieNode> subNodes = new HashMap<>();

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

        // 获取子节点
        public TrieNode getSubNodes(Character c) {
            return subNodes.get(c);
        }

        // 添加子节点的方法
        public void addSubNodes(Character c, TrieNode node) {
            subNodes.put(c, node);
        }
    }
}

创建敏感词文件

sensitive-words.txt

赌博
嫖娼
吸毒

测试功能是否生效

@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class SensitiveFilterTest {
    @Autowired
    private SensitiveFilter sensitiveFilter;

    @Test
    public void testSensitiveFilter(){
        String text="我要吸毒,嫖娼,赌博";
        text=sensitiveFilter.filter(text);
        System.out.println(text);

        text="我要※吸※毒※,※嫖※娼※,※赌※博※";
        text=sensitiveFilter.filter(text);
        System.out.println(text);
    }
}

效果
在这里插入图片描述

2、发布帖子

在这里插入图片描述

点击主页的发布帖子,发送ajax请求
在这里插入图片描述

  1. 引入fastjson
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
</dependency>
  1. 在工具类中编写返回json对象方法

CommunityUtil

/**
     * 返回json格式对象
     * @param code
     * @param msg
     * @param map
     * @return
     */
    public static String getJSONString(int code, String msg, Map<String,Object> map){
        JSONObject json = new JSONObject();
        json.put("code",code);
        json.put("msg",msg);
        if(map!=null){
            for (String key:map.keySet()){
                json.put(key,map.get(key));
            }
        }
        return json.toJSONString();
    }

    public static String getJSONString(int code, String msg){
        return getJSONString(code,msg,null);
    }

    public static String getJSONString(int code){
        return getJSONString(code,null,null);
    }
  1. 业务开发

DiscussPostMapper

//增加帖子的方法
int insertDiscussPost(DiscussPost discussPost);

DiscussPostMapper.xml

<sql id="insertFields">
    user_id,title,content,type,status,create_time,comment_count,score
</sql>

<insert id="insertDiscussPost" parameterType="DiscussPost">
    insert into discuss_post(<include refid="insertFields"></include>)
    values (#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>

DiscussPostService

/**
     * 新增帖子
     * @param post
     * @return
     */
    public int addDiscussPost(DiscussPost post){
        if(post==null){
            throw new IllegalArgumentException("参数不能为空");
        }
        // 转义html标签
        post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
        post.setContent(HtmlUtils.htmlEscape(post.getContent()));
        // 过滤敏感词
        post.setTitle(sensitiveFilter.filter(post.getTitle()));
        post.setContent(sensitiveFilter.filter(post.getContent()));

        return discussPostMapper.insertDiscussPost(post);
    }

DiscussController

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
    @Autowired
    private DiscussPostService discussPostService;
    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title, String content) {
        User user = hostHolder.getUser();
        if (user == null) {
            return CommunityUtil.getJSONString(403, "你还没有登录");
        }
        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());

        discussPostService.addDiscussPost(post);
        // 报错的请求,将来统一处理
        return CommunityUtil.getJSONString(0, "发布成功!");
    }
}
  1. 处理前端页面

index.html
在这里插入图片描述

index.js

$(function(){
	$("#publishBtn").click(publish);
});

function publish() {
	$("#publishModal").modal("hide");

	//获取标题和内容  id选择器
	var title = $("#recipient-name").val();
	var content = $("#message-text").val();

	//发送异步请求
	$.post(
		//CONTEXT_PATH+"/discuss/add",
		"/community/discuss/add",
		{"title": title, "content": content},
		function (data) {
			console.log(data);
			data = $.parseJSON(data);
			//在提示框中显示返回消息
			$("#hintBody").text(data.msg);
			//显示提示框
			$("#hintModal").modal("show");
			//2秒后。自动隐藏提示框
			setTimeout(function () {
				$("#hintModal").modal("hide");
				//刷新页面
				if (data.code == 0) {
					window.location.reload();
				}
			}, 2000);
		}
	);
}

3、帖子详情

在这里插入图片描述

3.1 DiscussPostMapper

//查询帖子
DiscussPost selectDiscussPostById(int id);

DiscussPostMapper.xml

<select id="selectDiscussPostById" resultType="DiscussPost">
    select <include refid="selectFields"></include>
    from discuss_post
    where id=#{id}
</select>

3.2 DiscussPostService

//根据ID查询帖子
public DiscussPost findDiscussPostById(int id){
    return mapper.selectDiscussPostById(id);
}

3.3 DiscussPostController

//根据id查询帖子
@RequestMapping(value = "/detail/{discussPostId}",method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
    //帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post",post);
    //关联查询
    User user =userService.findUserById(post.getUserId());
    System.out.println(user);
    model.addAttribute("user",user);
    return "/site/discuss-detail";
}

3.4 index.html

index.html
在这里插入图片描述

discuss-detail.html

在这里插入图片描述

4、显示评论

在这里插入图片描述

4.1 数据层

Comment

/**
 * 评论表
 */
@Data
public class Comment {
    //主键
    private int id;
    //用户id  谁发布的
    private int userId;
    // 评论的目标,帖子或评论、用户。。。
    private int entityType;
    // 目标id 帖子
    private int entityId;
    // 目标
    private int targetId;
    // 内容
    private String content;
    //状态 0:可用 1:禁用
    private int status;
    //时间
    private Date createTime;
}

CommentMapper

@Mapper
public interface CommentMapper {
    // 根据实体查询评论
    List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
    // 根据实体查询总数
    int selectCountByEntity(int entityType,int entityId);
}

CommentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.peng.dao.CommentMapper">

    <sql id="selectFields">
        id,user_id,entity_type,entity_id,target_id,content,status,create_time
    </sql>

    <select id="selectCommentsByEntity" resultType="Comment">
        select
        <include refid="selectFields"></include>
        from comment
        where status=0
        and entity_type=#{entityType}
        and entity_id=#{entityId}
        order by create_time asc
        limit #{offset},#{limit}
    </select>

    <select id="selectCountByEntity" resultType="int">
        select count(id)
        from comment
        where status = 0
        and entity_type = #{entityType}
        and entity_id = #{entityId}

    </select>
</mapper>

4.2 业务层

CommentService

@Service
public class CommentService {

    @Autowired
    private CommentMapper commentMapper;

    // 根据实体查询评论
    public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
        return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
    }

    // 根据实体查询总数
    public int findCommentCountByEntity(int entityType, int entityId) {
        return commentMapper.selectCountByEntity(entityType, entityId);
    }
}

4.3 表现层

DiscussPostController

/**
     * 根据id查询帖子
     *
     * @param discussPostId
     * @param model
     * @return
     */
    @RequestMapping(value = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
        // 查询帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post", post);
        // 作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);

        // 评论分页信息
        page.setLimit(5);
        page.setPath("/discuss/detail/" + post.getId());
        page.setRows(post.getCommentCount());

        // 评论:给帖子的评论
        // 回复:给帖子的评论
        // 评论列表
        List<Comment> commentList = commentService.
                findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
        // 返回结果
        List<Map<String, Object>> commentVoList = new ArrayList<>();

        if (commentList != null) {
            for (Comment comment : commentList) {
                // 一个评论的VO
                Map<String, Object> commentVo = new HashMap<>();
                // 添加评论
                commentVo.put("comment", comment);
                // 添加作者
                commentVo.put("user", userService.findUserById(comment.getUserId()));
                // 回复列表(评论的评论)
                List<Comment> replyList = commentService
                        .findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
                // 回复Vo列表
                List<Map<String, Object>> replyVoList = new ArrayList<>();
                if (replyList != null) {
                    for (Comment reply : replyList) {
                        Map<String, Object> replyVo = new HashMap<>();
                        // 回复
                        replyVo.put("reply", reply);
                        // 作者
                        replyVo.put("user", userService.findUserById(reply.getUserId()));
                        // 回复的目标
                        User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
                        replyVo.put("target", target);

                        // 存入集合
                        replyVoList.add(replyVo);
                    }
                }
                // 把回复存入commentVo
                commentVo.put("replys", replyVoList);
                System.out.println(replyVoList);
                // 回复数量
                int replyCount = commentService.findCommentCountByEntity(ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("replyCount", replyCount);

                // 存入集合
                commentVoList.add(commentVo);

            }
        }
        model.addAttribute("comments", commentVoList);

        return "/site/discuss-detail";
    }

index.html

在这里插入图片描述

discuss-detail.html

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
分页的复用

在这里插入图片描述

5、添加评论

在这里插入图片描述

5.1 数据层

CommentMapper

@Mapper
public interface CommentMapper {
    int insertComment(Comment comment);
}

CommentMapper.xml

<insert id="insertComment" parameterType="Comment" keyProperty="id">
    insert into comment(<include refid="insertFields"></include>)
    values (#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>

5.2 业务层

CommentService

// 添加评论
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int insertComment(Comment comment) {
        // 判空
        if (comment == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        // 转义标签
        comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
        // 过滤敏感词
        comment.setContent(sensitiveFilter.filter(comment.getContent()));
        // 添加评论
        int rows = commentMapper.insertComment(comment);

        // 更新评论数量
        if (comment.getEntityType() == ENTITY_TYPE_POST) {
            // 获取原来的评论数量
            int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
            // 更新评论数量
            discussPostService.updateCommentCount(comment.getEntityId(), count);
        }
        return rows;
    }

5.3 表现层

CommentController

@Controller
@RequestMapping("/comment")
public class CommentController {

    @Autowired
    private CommentService commentService;
    @Autowired
    private HostHolder hostHolder;

    /**
     * 添加评论
     * @param discussPostId
     * @param comment
     * @return
     */
    @RequestMapping(value = "/add/{discussPostId}",method = RequestMethod.POST)
    public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment){
        comment.setUserId(hostHolder.getUser().getId());
        comment.setStatus(0);
        comment.setCreateTime(new Date());

        commentService.insertComment(comment);

        return "redirect:/discuss/detail/"+discussPostId;
    }

}

discuss-detail.html

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6、私信列表

在这里插入图片描述

6.1 私信列表

数据持久层

Message

/**
 * 私信表
 */
@Data
public class Message {

    private int id;
    private int fromId;
    private int toId;
    private String conversationId;
    private String content;
    // 0:未读 1:已读 2:删除
    private int status;
    private Date createTime;
}

MessageMapper

@Mapper
public interface MessageMapper {

    // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
    List<Message> selectConversations(int userId,int offset,int limit);

    // 查询当前用户的会话数量
    int selectConversationCount(int userId);

    // 查询某个会话所包含的私信列表
    List<Message> selectLetters(String conversationId,int offset,int limit);

    // 查询某个会话所包含的私信数量
    int selectLetterCount(String conversationId);

    // 查询未读私信的数量
    int selectLetterUnreadCount(int userId,String conversationId);
}

MessageMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.MessageMapper">

    <sql id="selectFields">
        id,from_id,to_id,conversation_id,content,status,create_time
    </sql>

    <select id="selectConversations" resultType="Message">
        select
        <include refid="selectFields"></include>
        from message
        where id in(
        SELECT MAX(id) FROM message
        WHERE STATUS!=2 AND from_id!=1 AND(from_id = #{userId} or to_id=#{userId})
        GROUP BY conversation_id
        )
        order by id desc
        limit #{offset},#{limit}
    </select>

    <select id="selectConversationCount" resultType="int">
        select count(m.maxid)
        from (
                 SELECT MAX(id) maxid
                 FROM message
                 WHERE STATUS != 2
                   AND from_id != 1
                   AND (from_id = #{userId} or to_id = #{userId})
                 GROUP BY conversation_id
             ) as m
    </select>

    <select id="selectLetters" resultType="Message">
        select
        <include refid="selectFields"></include>
        from message
        where status!=2 and from_id !=1 and conversation_id =#{conversationId}
        order by id desc
        limit #{offset},#{limit}

    </select>

    <select id="selectLetterCount" resultType="int">
        select count(id)
        from message
        where status != 2
          and from_id != 1
          and conversation_id = #{conversationId}
    </select>


    <select id="selectLetterUnreadCount" resultType="int">
        select count(id)
        from message
        where status=0 and from_id!=1 and to_id=#{userId}
        <if test="conversationId!=null">
            and conversation_id =#{conversationId}
        </if>
    </select>
</mapper>

测试mapper.xml文件中的sql是否有误

/**
     * MessageMapper
     */
    @Test
    public void testSelectLetters(){
        List<Message> messages = messageMapper.selectConversations(112, 0, 10);
        for (Message message : messages) {
            System.out.println(message);
        }

        System.out.println("------------------");
        int count = messageMapper.selectConversationCount(112);
        System.out.println("数量为:"+count);

        System.out.println("------------------");
        List<Message> messageList = messageMapper.selectLetters("112_114", 0, 10);
        for (Message message : messageList) {
            System.out.println(message);
        }

        System.out.println("***************");
        int count1 = messageMapper.selectLetterCount("112_114");
        System.out.println("数量为:"+count1);

        int count2 = messageMapper.selectLetterUnreadCount(112, "114_112");
        System.out.println(count2);
    }

数据业务层

MessageService

@Service
public class MessageService {

    @Autowired
    private MessageMapper messageMapper;

    // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
    public List<Message> findConversations(int userId,int offset,int limit){
        return messageMapper.selectConversations(userId, offset, limit);
    }

    // 查询当前用户的会话数量
    public int findConversationCount(int useId){
        return messageMapper.selectConversationCount(useId);
    }

    // 查询某个会话所包含的私信列表
    public List<Message> findLetters(String conversationId,int offset,int limit){
        return  messageMapper.selectLetters(conversationId,offset,limit);
    }

    // 查询某个会话所包含的私信数量
    public int findLetterCount(String conversationId){
        return messageMapper.selectLetterCount(conversationId);
    }

    // 查询未读私信的数量
    public int  findLetterUnreadCount(int userId,String conversationId){
        return messageMapper.selectLetterUnreadCount(userId, conversationId);
    }
}

数据表现层

MessageController

@Controller
public class MessageController {

    @Autowired
    private MessageService messageService;
    @Autowired
    private HostHolder hostHolder;
    @Autowired
    private UserService userService;

    /**
     * 私信列表
     *
     * @param model
     * @param page
     * @return
     */
    @RequestMapping(value = "/letter/list", method = RequestMethod.GET)
    public String getLetterList(Model model, Page page) {
        User user = hostHolder.getUser();
        // 分页信息
        page.setLimit(5);
        page.setPath("/letter/list");
        page.setRows(messageService.findConversationCount(user.getId()));

        // 会话列表
        List<Message> conversationList = messageService
                .findConversations(user.getId(), page.getOffset(), page.getLimit());

        List<Map<String, Object>> conversations = new ArrayList<>();
        if (conversationList != null) {
            for (Message message : conversationList) {
                Map<String, Object> map = new HashMap<>();
                map.put("conversation", message);
                map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
                map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
                int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
                map.put("target", userService.findUserById(targetId));

                conversations.add(map);
            }
        }
        model.addAttribute("conversations", conversations);

        // 查询未读消息数量
        int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
        model.addAttribute("letterUnreadCount", letterUnreadCount);
        return "/site/letter";
    }
}

index.html

点击消息页面跳转到letter.html私信列表页面
在这里插入图片描述
letter.html

引入thymeleaf模板以及静态资源路径的绑定后
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
返回的js

<script>
    function back() {
        location.href = CONTEXT_PATH + "/letter/list";
    }
</script>

6.2 私信详情

MessageController

/**
     * 私信详情
     * @param conversationId
     * @param page
     * @param model
     * @return
     */
    @RequestMapping(value = "/letter/detail/{conversationId}", method = RequestMethod.GET)
    public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
        // 分页信息
        page.setLimit(5);
        page.setPath("/letter/detail/" + conversationId);
        page.setRows(messageService.findLetterCount(conversationId));

        // 获取私信信息
        List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
        List<Map<String, Object>> letters = new ArrayList<>();
        if (letterList != null) {
            for (Message message : letterList) {
                Map<String, Object> map = new HashMap<>();
                map.put("letter", message);
                map.put("fromUser", userService.findUserById(message.getFromId()));

                letters.add(map);
            }
        }
        model.addAttribute("letters", letters);
        // 私信目标
        model.addAttribute("target", getLetterTarget(conversationId));

        return "/site/letter-detail";
    }

    /**
     * 获取目标对象
     *
     * @param conversationId
     * @return
     */
    private User getLetterTarget(String conversationId) {
        String[] ids = conversationId.split("_");
        int id0 = Integer.parseInt(ids[0]);
        int id1 = Integer.parseInt(ids[1]);

        if (hostHolder.getUser().getId() == id0) {
            return userService.findUserById(id1);
        } else {
            return userService.findUserById(id0);
        }
    }

letter.html
在这里插入图片描述

letter-detail.html
在这里插入图片描述
在这里插入图片描述

7、发送私信

7.1 发送私信

数据持久层

MessageMapper

//添加私信
int insertMessage(Message message);

//修改消息的状态
int updateStatus(List<Integer> ids,int status);

MessageMapper.xml

<insert id="insertMessage" keyProperty="id" parameterType="Message">
    insert into message (<include refid="insertFields"></include>)
    values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime});
</insert>

<update id="updateStatus">
    update message
    set status=#{status}
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</update>

数据业务层

MessageService

public int addMessage(Message message){
    message.setContent(HtmlUtils.htmlEscape(message.getContent()));
    message.setContent(sensitiveFilter.filter(message.getContent()));
    return messageMapper.insertMessage(message);
}

public int readMessage(List<Integer> ids){
    return messageMapper.updateStatus(ids,1);
}

数据表现层

MessageController

@RequestMapping(value = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content) {
    User target = userService.findUserByName(toName);
    System.out.println("target--------------"+target);
    if (target == null) {
        return CommunityUtil.getJsonString(1, "用户不存在");
    }

    Message message = new Message();
    message.setFromId(hostHolder.getUser().getId());
    message.setToId(target.getId());
    if (message.getFromId() < message.getToId()) {
        message.setConversationId(message.getFromId() + "_" + message.getToId());
    } else {
        message.setConversationId(message.getToId() + "_" + message.getFromId());
    }
    message.setContent(content);
    message.setCreateTime(new Date());
    messageService.addMessage(message);
    System.out.println("controller执行了");
    return CommunityUtil.getJsonString(0);
}

letter.js

$(function(){
   $("#sendBtn").click(send_letter);
   $(".close").click(delete_msg);
});

function send_letter() {
   $("#sendModal").modal("hide");

   var toName=$("#recipient-name").val();
   var content=$("#message-text").val();
   $.post(
      //CONTEXT_PATH+"/letter/send",
      "/community/letter/send",
      {"toName":toName,"content":content},
      function (data){
         data=$.parseJSON(data);
         if (data.code==0){
            $("hintBody").text("发送成功!");
         }else {
            $("hintBody").text(data.msg);
         }

         $("#hintModal").modal("show");
         setTimeout(function(){
            $("#hintModal").modal("hide");
            location.reload();
         }, 2000);
      }
   )
}

function delete_msg() {
   // TODO 删除数据
   $(this).parents(".media").remove();
}

7.2 设置已读

MessageController

/**
     * 获取未读消息id
     * @param letterList
     * @return
     */
    private List<Integer> getLetterIds(List<Message> letterList) {
        List<Integer> ids = new ArrayList<>();

        if (letterList != null) {
            for (Message message : letterList) {
                if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
                    ids.add(message.getId());
                }
            }
        }
        return ids;
    }

在这里插入图片描述

8、统一异常处理

在这里插入图片描述

将error文件夹放在templates文件下,并引入thymeleaf模板
在这里插入图片描述

获取错误页面

HomeController

@RequestMapping(value = "/error",method = RequestMethod.GET)
public String getErrorPage(){
    return "/error/500";
}

捕捉controller异常

ExceptionAdvice

@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
    // 日志
    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    @ExceptionHandler({Exception.class})
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常:"+e.getMessage());
        for (StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }
        // 如果是异步请求就返回json字符串,否则返回错误页面
        String xRequestWith = request.getHeader("x-request-with");
        if("XMLHttpRequest".equals(xRequestWith)){
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer=response.getWriter();
            writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
        }else{
            response.sendRedirect(request.getContextPath()+"/error");
        }
    }
}

9、统一记录日志

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

把所有的业务组件进行记录日志,在业务组件调用的一开始进行记录

导包

<!--aspect-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

ServiceLogAspect

@Component
@Aspect
public class ServiceLogAspect {
    // 日志
    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

    @Pointcut("execution(* com.example.community.service.*.*(..))")
    public void pointcut(){

    }

    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        // 用户。。。在。。。访问。。。
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteHost();
        String now = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
        String target = joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName();

        logger.info(String.format("用户在[%s],在[%s],访问了[%s]",ip,now,target));
    }
}

效果

在这里插入图片描述

第3章 Redis 一站式高性能存储方案

在这里插入图片描述常用基本命令

select 1 # 选择数据库

flushdb # 清空数据库

keys * # 获取所有数据

type key # 查看数据类型

exists key # 当前key是否存在

expire key second # 几秒后删除

del key # 删除key

strings

set key value # 存值

get key # 取值

incr key # 增加value值

decr key # 减少value值

hashs

hset key field value # 存值  键值对

hget key field # 取值

lists

lpush key value1 value2 valu3 # 存值(入栈)

llen key # 列表长度

lindex key index # 对应索引的值

lrange key star end # 范围性取值

rpop key # 出栈(右侧)

sets

sadd key value1 value2 value3 # 存值

scard key # 集合中的数量

spop key # 随机抽取一个值 弹出,  抽奖

smembers key # 遍历当前集合

zsets

zadd key score member socre member # 存值

zcard key # 数量

zscore key member # 取值(分数)

zrank key member # 	返回有序集合中指定成员的索引

zrange key star end # 遍历有序集合

1、Spring整合Redis

在这里插入图片描述

1.1 引入依赖

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

1.2 配置Redis

在spring对redis的自动配置中,默认redis 的key为Object类型。我们需要将redis的key设置为String类型的
在这里插入图片描述
application.yml

# redis
spring:
  redis:
    database: 11
    host: localhost
    port: 6379

1.3 Redis序列化

将redis中key的类型转化为string

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 实例化
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(factory);

        // 设置key序列化方式
        redisTemplate.setKeySerializer(RedisSerializer.string());
        // 设置普通的value序列化方式
        redisTemplate.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化
        redisTemplate.setHashValueSerializer(RedisSerializer.json());
        // 设置生效
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

1.4 测试redis

import com.example.community.CommunityApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.TimeUnit;


@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * string
     */
    @Test
    public void testRedis(){
        String redisKey = "test:count";

        redisTemplate.opsForValue().set(redisKey,1);
        System.out.println(redisTemplate.opsForValue().get(redisKey));
        System.out.println(redisTemplate.opsForValue().increment(redisKey));
        System.out.println(redisTemplate.opsForValue().decrement(redisKey));

    }

    /**
     * hash
     */
    @Test
    public void testHashes(){
        String redisKey="test:user";

        redisTemplate.opsForHash().put(redisKey,"id",1);
        redisTemplate.opsForHash().put(redisKey,"name","张三");

        System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
        System.out.println(redisTemplate.opsForHash().get(redisKey, "name"));
    }

    /**
     * list
     */
    @Test
    public void testList(){
        String redisKey="test:ids";

        redisTemplate.opsForList().leftPush(redisKey,101);
        redisTemplate.opsForList().leftPush(redisKey,102);
        redisTemplate.opsForList().leftPush(redisKey,103);

        System.out.println(redisTemplate.opsForList().size(redisKey));
        System.out.println(redisTemplate.opsForList().index(redisKey, 0));
        System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));

        System.out.println(redisTemplate.opsForList().leftPop(redisKey));
        System.out.println(redisTemplate.opsForList().leftPop(redisKey));
        System.out.println(redisTemplate.opsForList().leftPop(redisKey));

    }
    /**
     * set
     */
    @Test
    public void testSets(){
        String redisKey="test:teachers";

        redisTemplate.opsForSet().add(redisKey,"刘备","张飞","关羽");

        System.out.println(redisTemplate.opsForSet().size(redisKey));
        System.out.println(redisTemplate.opsForSet().pop(redisKey));
        System.out.println(redisTemplate.opsForSet().members(redisKey));
    }

    /**
     * sortedSets
     */
    @Test
    public void testZSet(){
        String redisKey="test:students";

        redisTemplate.opsForZSet().add(redisKey,"唐僧",10);
        redisTemplate.opsForZSet().add(redisKey,"孙悟空",99);
        redisTemplate.opsForZSet().add(redisKey,"猪八戒",80);
        redisTemplate.opsForZSet().add(redisKey,"沙和尚",70);
        redisTemplate.opsForZSet().add(redisKey,"白龙马",60);

        System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
        System.out.println(redisTemplate.opsForZSet().score(redisKey, "唐僧"));
        System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "唐僧"));
        System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, -1));
    }

    /**
     * key是否存在
     */
    @Test
    public void  testKeys(){
        redisTemplate.delete("test:user");

        System.out.println(redisTemplate.hasKey("test:user"));
        redisTemplate.expire("test:teachers",10, TimeUnit.SECONDS);
    }

    /**
     * 多吹访问同一个key
     */
    @Test
    public void testBoundOperations(){
        String redisKey="test:count";

        BoundValueOperations<String, Object> operations = redisTemplate.boundValueOps(redisKey);
        operations.increment();
        operations.increment();
        System.out.println(operations.get());
    }


}

1.5 redis中的事务

概念

  • redis事务本质是一组命令的集合。事务支持一次性执行多个命令,一个事务中所有命令都会被序列化。
  • 在事务执行过程中,会按照顺序串行化执行队列中的命令,其他科幻的提交的命令请求不会插入到事务执行命令序列中。
  • 简单来说:redis事务就是一次性、顺序性、排他性、的执行一个队列中的一系列命令。

不存在隔离级别

  • 批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

不能保证原子性

  • Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

事务执行的三个阶段

  • 开始事务
  • 命令入队
  • 执行事务

相关命令

  • watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
  • multi : 标记一个事务块的开始( queued )
  • exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
  • discard : 取消事务,放弃事务块中的所有命令
  • unwatch : 取消watch对所有key的监控

简单使用

/**
     * 编程式事务
     */
    @Test
    public void testTransactional(){
        Object obj=redisTemplate.execute(new SessionCallback() {
            @Override
            public  Object execute(RedisOperations operations) throws DataAccessException {
                String redisKey = "test:tx";
                // 开启事务
                operations.multi();
                operations.opsForSet().add(redisKey,"张三");
                operations.opsForSet().add(redisKey,"李四");
                operations.opsForSet().add(redisKey,"王五");
                // 此时事务未被提交,所以为空[]
                System.out.println(operations.opsForSet().members(redisKey));

                // 提交事务
                return operations.exec();
            }
        });

        System.out.println(obj);//[1, 1, 1, [王五, 张三, 李四]]
    }

2、点赞

在这里插入图片描述

2.1 点赞

  1. 编写保存redisKey的工具类

RedisKeyUtil

/**
 * 生成需要的Key
 */
public class RedisKeyUtil {

    private static final String SPLIT = ":"; // 分隔符
    private static final String PREFIX_ENTITY_LIKE = "like:entity"; //点赞实体

    // 某个实体的赞   like:entity:entityType:entityId -> set(userId)
    public static String getEntityLikeKey(int entityType,int entityId){
        return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;
    }
}
  1. 在业务层创建LikeService用来创建使用redis的方法

LikeService

@Service
public class LikeService {

    @Autowired
    private RedisTemplate redisTemplate;

    // 点赞
    public void like(int userId, int entityType, int entityId) {
        // 获取key
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        // 查看是否已存在集合中
        Boolean isMember = redisTemplate.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;
    }
}

  1. 实现点赞功能

LikeController

@Controller
public class LikeController {

    @Autowired
    private LikeService likeService;

    @Autowired
    private HostHolder hostHolder;

    /**
     * 点赞功能
     * @param entityType
     * @param entityId
     * @return
     */
    @RequestMapping(value = "/like",method = RequestMethod.POST)
    @ResponseBody
    public String like(int entityType,int entityId){
        User user =hostHolder.getUser();

        // 点赞
        likeService.like(user.getId(),entityType,entityId);
        // 数量
        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);
    }
}

disscuss-detail.html

作者
在这里插入图片描述
回帖列表

在这里插入图片描述

回复列表

在这里插入图片描述

discuss.js 记得引入js

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

2.2 首页点赞数量

HomeContoller

在这里插入图片描述

index.html

在这里插入图片描述

2.3 详情页点赞数量

DiscussPostController

帖子点赞数量以及状态

在这里插入图片描述

回帖点赞数量以及状态

在这里插入图片描述

回复点赞数量以及状态

在这里插入图片描述

3、我收到的赞

在这里插入图片描述

3.1 重构点赞功能

编写用户赞的key,用来记录点赞数量

RedisKeyUtil

 	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;
    }

likeService中加入用户点赞数量的方法,由于两个方法要么同时成功,要么同时失败,所以利用编程式事务,entityUserId为被点赞id,userId为点赞者id
在这里插入图片描述
LikeController的点赞功能中添加一个EntityUserId参数
在这里插入图片描述
在帖子详情discuss-detail页面添加一个entityUserId参数
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

discuss.js

在这里插入图片描述

3.2 开发个人主页

LikeService

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

UserController

/**
     * 个人主页
     * @param userId
     * @param model
     * @return
     */
    @RequestMapping(value = "/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";
    }

index.html

点击头部栏头像的下拉框的个人主页即可跳转
在这里插入图片描述
点击帖子列表中的头像即可跳转
在这里插入图片描述

discuss-detail.html

点击作者头像跳转到主页
在这里插入图片描述
点击评论中头像跳转主页
在这里插入图片描述
profile.html

引入thymeleaf模板
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、关注、取消关注

在这里插入图片描述

4.1关注、取消关注

RedisKeyUtil

//关注
private static final String PREFIX_FOLLOWEE = "followee"; //关注目标
private static final String PREFIX_FOLLOWER = "follower"; //粉丝


// 某个关注的实体
// 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

// 关注、取消关注
@Service
public class FollowService {

    @Autowired
    private RedisTemplate redisTemplate;

    // 关注
    public void follow(int userId, int entityType, int entityId) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityId);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

                redisOperations.multi();

                redisOperations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
                redisOperations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

                return redisOperations.exec();
            }
        });
    }

    // 取消关注
    public void unfollow(int userId, int entityType, int entityId) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityId);
                String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

                redisOperations.multi();

                redisOperations.opsForZSet().remove(followeeKey, entityId);
                redisOperations.opsForZSet().remove(followerKey, userId);
                return redisOperations.exec();
            }
        });
    }
}

FollowController

// 关注、取消关注
@Controller
public class FollowController {

    @Autowired
    private FollowService followService;
    @Autowired
    private HostHolder hostHolder;

    // 关注
    @RequestMapping(value = "/follow", method = RequestMethod.POST)
    @ResponseBody
    public String follow(int entityType, int entityId) {
        User user = hostHolder.getUser();

        followService.follow(user.getId(), entityId, entityType);

        return CommunityUtil.getJsonString(0, "已关注!");
    }

    // 取消关注
    @RequestMapping(value = "/unfollow", method = RequestMethod.POST)
    @ResponseBody
    public String unfollow(int entityType, int entityId) {
        User user = hostHolder.getUser();

        followService.unfollow(user.getId(), entityId, entityType);

        return CommunityUtil.getJsonString(0, "已取消关注!");
    }
}

profile.html

点击按钮进行关注、取消关注
在这里插入图片描述
profile.js

$(function(){
    $(".follow-btn").click(follow);
});

function follow() {
    var btn = this;
    var entityId = $(btn).prev().val();

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

        // $(btn).removeClass("btn-secondary").addClass("btn-info");
    }
}

4.2 用户的关注数、粉丝数

FollowService

// 查询某个用户关注的实体数量
    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
在这里插入图片描述
profile.html
在这里插入图片描述

5、关注列表与粉丝列表

在这里插入图片描述

5.1 业务层

FollowService

// 查询某用户关注的人
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()));
        System.out.println(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()));
        System.out.println(new Date(score.longValue()));
        list.add(map);
    }
    return list;
}

5.2 表现层

FollowController

// 用户关注的人
@RequestMapping(value = "/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";
}

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

// 粉丝
@RequestMapping(value = "/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";
}

profile.html
在这里插入图片描述
follower.html
在这里插入图片描述
在这里插入图片描述
followee.html

follower.html 一样

6、优化登录模块

在这里插入图片描述

6.1 使用Redis存储验证码

1. 设置验证码的KeyRedisKeyUtil

//验证码
private static final String PREFIX_KAPTCHA = "kaptcha";

// 验证码
public static String getCaptchaKey(String owner) {
    return PREFIX_KAPTCHA + SPLIT + owner;
}

2. 将原来的session存储改为存入redisLoginController

获取图片验证码

		// 1.获取随机字符串
        String kaptchaOwner = CommunityUtil.generateUUID();
        // 2.验证码的归属
        Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
        cookie.setMaxAge(60);// 生效时间
        cookie.setPath(contextPath);// 生效路径
        response.addCookie(cookie);
        // 3.将验证码存入redis
        String redisKey = RedisKeyUtil.getkaptchaKey(kaptchaOwner);
        // key: redisKey value :text  60秒有效
        redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);

用户登录

		// 1.检查验证码
        String kaptcha=null;
        // 2.判断随机字符串是否存在
        if(StringUtils.isNotBlank(kaptchaOwner)){
            // 如果存在,则获取redis中的验证码
            String redisKey = RedisKeyUtil.getkaptchaKey(kaptchaOwner);
            kaptcha= (String) redisTemplate.opsForValue().get(redisKey);
        }

在这里插入图片描述
在这里插入图片描述

4. 测试

成功存入redis
在这里插入图片描述

6.2 使用Redis存储登陆凭证

1. 设置登陆凭证的key

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

2. 将原来关于登录凭证的方法置为不可用

LoginTicketMapper
@Deprecated 不推荐使用

@Mapper
@Deprecated
public interface LoginTicketMapper {
}

3. 利用redis修改原来的方法

用户登录

//        loginTicketMapper.insertLoginTicket(loginTicket);
        // 将登陆凭证存入redis
        String redisKey = RedisKeyUtil.getLoginTicket(loginTicket.getTicket());
        // redis会把对象序列化为一个json
        redisTemplate.opsForValue().set(redisKey,loginTicket);

退出

/**
     * 退出
     *
     * @param ticket
     */
    public void logout(String ticket) {
        // 1.获取key
        String redisKey = RedisKeyUtil.getLoginTicket(ticket);
        // 2.获取登录凭证
        LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
        // 3.修改状态
        loginTicket.setStatus(1);
        // 4.重新存入
        redisTemplate.opsForValue().set(redisKey,loginTicket);
    }

在退出时有一个小小的bug,如果在退出之前,redis中没有存入ticket,则会报错(因为可能上次登陆后未退出,然后利用redis进行登录凭证的重构)

查询登录凭证

/**
     * 根据ticket查询登录凭证信息
     *
     * @param ticket
     * @return
     */
    public LoginTicket findLoginTicket(String ticket) {
        // 1.获取key
        String redisKey = RedisKeyUtil.getLoginTicket(ticket);
        // 2.获取登录凭证
        return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    }

4. 测试

在这里插入图片描述

6.3 使用redis缓存用户信息

1. 设置用户信息的key

    private static final String PREFIX_USER ="user"; // 用户信息
	// user
    public static String getUserKey(int userId){
        return PREFIX_USER+SPLIT+userId;
    }

2. 利用redis重构

UserService

封装三个关于redis的方法

// 1.优先去缓存中取值
    public User getCache(int userId){
        String redisKey=RedisKeyUtil.getUserKey(userId);
        User user= (User) redisTemplate.opsForValue().get(redisKey);
        return user;
    }

    // 2。取不到时初始化缓存数据
    public User initCache(int userId){
        // 获取 user
        User user=userMapper.selectById(userId);
        String redisKey=RedisKeyUtil.getUserKey(userId);
        // 将user存入redis缓存
        redisTemplate.opsForValue().set(redisKey,user);
        return user;
    }
    // 3.数据变更时清楚缓存数据
    public void clearCache(int userId){
        // 获取key
        String redisKey=RedisKeyUtil.getUserKey(userId);
        // 删除key
        redisTemplate.delete(redisKey);
    }

重构查询代码

 // 根据id查找user
    public User findUserById(int id) {
        //return userMapper.selectById(id);
        // 1.先去缓存中取数据
        User user = getCache(id);
        if (user==null){
            // 2.初始化user
            user = initCache(id);
        }
        return 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);
            // 清理redis缓存
            clearCache(userId);
            return ACTIVATION_SUCCESS;
        } else {
            return ACTIVATION_FAILURE;
        }
    }
    
    //更新用户头像
    public int updateHeader(int userId, String headerUrl) {
        //return userMapper.updateHeader(userId, headerUrl);
        int rows = userMapper.updateHeader(userId, headerUrl);
        clearCache(userId);
        return rows;
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 测试
在这里插入图片描述

本文含有隐藏内容,请 开通VIP 后查看