牛客社区项目

创建项目

认识Spring 

Spring Ioc

  • Inversion 偶发Control
    • 控制反转,是一种面向对象的设计思想。
  • Dependecy Injection
    • 依赖注入,是Ioc思想的实现方式。
  • Ioc Container
    • Ioc容器,是实现依赖注入的关键,本质上是一个工厂。

下面通过单元测试来认识上面提到的内容:

默认打开测试类是这样的——

当我们想要测试某个bean的时候,只需要给测试类添加上@ContenxtConfiguration注解来标注我们想要导入这个测试类的某些bean,详见@ContextConfiguration的用法_Kevin_Darcy的博客-CSDN博客

ApplicationContextAware:Aware接口的Bean在被初始化之后,可以获取到一些相对应的资源,Aware接口本身并不具备什么功能,一般用于子类继承后,Spring上下文初始化bean的时候对这个bean传入需要的资源。当一个类实现了ApplicationContextAware之后,这个类就能够得到ApplicationContext中的全部bean,换句话说,就是这个类能够直接获取spring配置文件中全部的bean对象。

当测试的接口被两个类同时实现时,若不处理的话会报异常,这个时候有两种做法:

一:在其中一个实现类上面加上@Primary注解

@Repository
@Primary
public class ExampleDaoMyBatis implements ExampleDao{
    @Override
    public String select() {
        return "MyBatis";
    }
}

二:在注解中给实现类起名字,然后通过类名来访问

@Repository("ExampleDaoBibernate")
public class ExampleDaoBibernate implements ExampleDao{
    @Override
    public String select() {
        return "hIbernate";
    }
}

applicationContext.getBean("ExampleDaoBibernate", ExampleDao.class);

当需要在Spring中使用别人写的类的时候,只需要将这个类配置到Spring中即可:

@Configuration
public class ExampleConfig {
    @Bean
    public SimpleDateFormat simpleDateFormat(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
}
	@Test
	public void testBeanConfig(){
		System.out.println(applicationContext.getBean(SimpleDateFormat.class).format(new Date()));
	}

认识Spring MVC

请求数据的两种方法

通过? 

    //GET请求
    //  /student?current=1&limit=20
    @RequestMapping(path = "/students",method = RequestMethod.GET)
    @ResponseBody
    public String getStudents(
            @RequestParam(name = "current",required = false,defaultValue = "1") int Current,
            @RequestParam(name = "limit",required = false,defaultValue = "10") int Limit) {
        System.out.println(Current);
        System.out.println(Limit);
        return "some student";
    }

 @RequestParam注解中的name是指将前端中current的值传递给后端Current中,required默认为true表示前端必须传递值,如果不穿就会报错,因为我们不知道前端是否要传递值,所以改为false,同时如果前端没有传递值,那么我们就设定一个默认值作为前端传递的值。

通过/ 

    //  /student/123
    @RequestMapping(path = "/student/{id}",method = RequestMethod.GET)
    @ResponseBody
    public String getStudent(@PathVariable("id") int id){
        System.out.println(id);
        return "a student";
    }

 上面的这种方式是Restful风格的写法

响应数据的各种方式

返回属性 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>增加学生</title>
</head>
<body>
<form method="post" action="/community/example/student">
  <p>
    姓名:<input type="text" name="name">
  </p>
  <p>
    年龄:<input type="text" name="age">
  </p>
  <p>
    <input type="submit" value="保存">
  </p>
</body>
</form>

</html>

    //POST请求
    @RequestMapping(path = "/student",method = RequestMethod.POST)
    @ResponseBody
    public String saveStudent(String name,int age){
        System.out.println(name);
        System.out.println(age);
        return "success";
    }

返回HTML页面

    //响应HTML数据
    @RequestMapping(path = "/teacher",method = RequestMethod.GET)
    public ModelAndView getTeacher(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("name","张三");
        modelAndView.addObject("age",30);
        modelAndView.setViewName("/demo/view");
        return modelAndView;
    }

 

另外一种方式:

    @RequestMapping(path = "/school",method = RequestMethod.GET)
    public String getSchool(Model model){
        model.addAttribute("name","西华大学");
        model.addAttribute("age",80);
        return "/demo/view";
    }

 返回JSON数据

    @RequestMapping(path = "/emp",method = RequestMethod.GET)
    @ResponseBody
    public Map<String, Object> getEmp(){
        Map<String,Object> emp = new HashMap<>();
        emp.put("name","张三");
        emp.put("age",23);
        emp.put("salary",9000);
        return emp;
    }

    @RequestMapping(path = "/emps",method = RequestMethod.GET)
    @ResponseBody
    public List<Map<String, Object>> getEmps(){
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String,Object> emp = new HashMap<>();
        emp.put("name","张三");
        emp.put("age",23);
        emp.put("salary",9000);
        list.add(emp);

        emp.put("name","李四");
        emp.put("age",24);
        emp.put("salary",9300);
        list.add(emp);

        emp.put("name","王五");
        emp.put("age",25);
        emp.put("salary",8000);
        list.add(emp);
        return list;
    }

注册功能

访问注册页面

@Controller
public class LoginController {
    @RequestMapping(path = "/register",method = RequestMethod.GET)
    public String getRegisterPage(){
        return "/site/register";
    }
}

 提交注册数据

为了判断字符串和集合中的数据是否为空方便后面编程的需要,需要导入一个包:

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

因为注册的时候需要发送邮件,邮件中需要激活链接,这个链接就需要链接到本机地址,所以需要在配置文件中自定义配置本机地址:

community.path.domain=http://localhost:8080

为了注册页面生成随机激活码和后面上传头像和设置密码,需要封装一个工具类:

public class CoumunityUtil {

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

    /**
     * MD5加密
     * hello —> abc123def456
     * hello + 3e4a8 -> abc123def456abc
     * @param key
     * @return
     */
    public static String md5(String key){
        if(StringUtils.isBlank(key)){
            return null;
        }
        /**
         * 作用:将传入的值加密成十六进制的字符串返回
         * DigestUtils是Spring自带的工具类
         */
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }
}

业务层:

@Service
public class UserService{

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TemplateEngine templateEngine;

    @Autowired
    private MailClient mailClient;

    @Value("${community.path.domain}")
    private String domain;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    public Map<String,Object> register(User user) throws IllegalAccessException {
        Map<String,Object> map = new HashMap<>();

        //空值处理
        if(user == null){
            throw new IllegalAccessException("参数不能为空");
        }

        if(StringUtils.isBlank(user.getUsername())){
            map.put("usernameMsg","账号不能为空!");
            return map;
        }
        if(StringUtils.isBlank(user.getPassword())){
            map.put("passwordMsg","密码不能为空!");
            return map;
        }
        if(StringUtils.isBlank(user.getEmail())){
            map.put("emailMsg","邮箱不能为空!");
            return map;
        }

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

        //注册用户
        user.setSalt(CoumunityUtil.generateUUID().substring(0,5));
        user.setPassword(CoumunityUtil.md5(user.getPassword()) + user.getSalt());
        user.setType(0);
        user.setStatus(0);
        user.setActivationCode(CoumunityUtil.generateUUID());
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
        user.setCreateTime(new Date());
        userMapper.insertUser(user);



        //激活邮件
        Context context = new Context();
        context.setVariable("email",user.getEmail());
        // 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;
    }
}

控制层:

@Controller
public class LoginController{

    @Autowired
    private UserService userService;

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

}

激活注册账号

为了判断邮件的注册状态需要封装一个激活状态类:

public interface ActiveStatus {
    /**
     * 激活成功
     */
    int ACTIVE_SUCCESS = 0;

    /**
     * 重复激活
     */
    int ACTIVE_REPEAT = 1;

    /**
     * 激活失败
     */
    int ACTIVE_FAILURE = 2;
}

业务层: 

@Service
public class UserService implements ActiveStatus {

    @Autowired
    private UserMapper userMapper;

    public int activetion(int userId,String code){
        User user = userMapper.selectById(userId);
        System.out.println(user.getStatus());
        if(user.getStatus() == 1){
            return ACTIVE_REPEAT;
        } else if (user.getActivationCode().equals(code)) {
            userMapper.updateStatus(userId,1);
            return ACTIVE_SUCCESS;
        }else{
            return ACTIVE_FAILURE;
        }
    }
}

控制层:

@Controller
public class LoginController implements ActiveStatus {

    @Autowired
    private UserService userService;

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

    @RequestMapping(path = "/login",method = RequestMethod.GET)
    public String login(){
        return "/site/login";
    }
}

 登录页面生成验证码功能 

生成验证码需要一个工具Kaptcha

 导入jia包:

		<dependency>
			<groupId>com.github.penggle</groupId>
			<artifactId>kaptcha</artifactId>
			<version>2.3.2</version>
		</dependency>

编写Kaptcha配置类:

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer DefaultKaptcha(){
        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","0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
        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;
    }
}

上面将Kaptcha包里面的Producer类装配到Spring容器中,Producer是一个接口,它里面就两个方法createImage(根据提供的文字生成图片)方法和createText(生成文字)方法:

DefaultKaptcha是Producer的实现类,它里面就有上面提到的两个方法,但是里面的参数配置比较麻烦,为了实现我们的目的,我们需要自己写一个配置 ,所以需要将配置类Properties对象创建出来,在里面写我们的逻辑,然后配置文件需要装配,所以创建出装配类Congif。最后再使用DefaultKaptcha里面的setConfig方法将配置对象穿进去,将我们的配置加入到Producer配置文件中。关于配置类Properties和装配类Config可以看下面这两篇文章。

(8条消息) Properties类的用法总结_源码复兴号的博客-CSDN博客

(8条消息) 独家解读:简单又强大的配置文件 Config 读写类_config类_曾经去过跨越一个小时的地方的博客-CSDN博客生成随机字符串生成图片

@Controller
public class LoginController implements ActiveStatus {
    @Autowired
    private Producer defaultKaptcha;

    @RequestMapping(path = "/kaptcha",method = RequestMethod.GET)
    public void generateKaptcha(HttpServletResponse response, HttpSession session){
        //生成字符串
        String text = defaultKaptcha.createText();
        //生成图片
        BufferedImage image = defaultKaptcha.createImage(text);
        //将text存储到session中
        session.setAttribute("Kaptcha",text);

        //将图片输出给浏览器
        response.setContentType("image/png");
        try {
            OutputStream outputStream = response.getOutputStream();
            ImageIO.write(image,"png",outputStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

开发登录、退出功能

开发功能 

因为登录页面有一个记住我功能,是否勾选它的设定缓存时间是不同的,所以需要把他们设置到常量中:

    /**
     * 设置默认登陆状态的超时时间
     */
    int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

    /**
     * 设置勾选记住我状态登录凭证的超时时间
     */
    int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;

 dao层:

@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以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。 useGeneratedKeys参数只针对 insert 语句生效,默认为 false
    int insertLoginTicket(LoginTicket loginTicket);

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

    @Update({
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
            "</script>"
    })
    int updateStatus(String ticket,int status);
}

上面注意一点自增主键的设置:

@Options(useGeneratedKeys = true,keyProperty = "id")    //表示如果插入的表id以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。 useGeneratedKeys参数只针对 insert 语句生效,默认为 false

业务层:

@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    public Map<String,Object> login(String username,String password,int expiredSeconds){
        Map<String,Object> map = new HashMap<>();
        //验证账号是否为空
        if(StringUtils.isBlank(username)){
            map.put("usernameMsg","账号不能为空");
            return map;
        }
        //验证密码是否为空
        if(StringUtils.isBlank(password)){
            map.put("passwordMsg","密码不能为空");
            return map;
        }

        //验证账号是否存在
        User user = userMapper.selectByName(username);
        if(user==null){
            map.put("usernameMsg","该账号不存在");
            return map;
        }
        //验证密码是否正确
        password = CoumunityUtil.md5(password + user.getSalt());
        if(!user.getPassword().equals(password)){
            map.put("passwordMsg","密码不正确");
            return map;
        }
        //验证激活状态
        if(user.getStatus()==0){
            map.put("usernameMsg","该账号未激活");
            return map;
        }

        //生成登录凭证
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CoumunityUtil.generateUUID());
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        loginTicketMapper.insertLoginTicket(loginTicket);

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

控制层:

@Controller
public class LoginController implements CommunityConstant {

    @Autowired
    private UserService userService;

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

退出功能

 业务层:

@Service
public class UserService implements CommunityConstant {
    @Autowired
    private LoginTicketMapper loginTicketMapper;

    public void logout(String ticket){
        loginTicketMapper.updateStatus(ticket, 1);
    }
}

控制层:

@Controller
public class LoginController implements CommunityConstant {
    @Autowired
    private UserService userService;

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

显示登录信息功能

页面要根据用户的登陆状态显示不同的信息,比如用户登陆以后页面中的导航栏就只能显示首页、消息、个人中心等功能键。用户未登录就要显示首页、注册、登录等功能。为了显示这样的就需要实现拦截器功能来判断用户的登录状态。之前在用户登录的业务里面当用户登录以后我们会给浏览器一个ticket,所以只要通过这个ticket凭证就能判断用户是否登录。

业务大致流程:

拦截器和拦截规则的示例 

@Component
public class AlphaInterceptor implements HandlerInterceptor {

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

    //在Controller之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("preHandle:" + handler.toString());
        return true;
    }

    //在Controller之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("postHandle:" + handler.toString());
    }

    //在TemplateEngin之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion" + handler.toString());
    }
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AlphaInterceptor alphaInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg")
                .addPathPatterns("/register","/login");
    }
}

配置拦截器和拦截规则

@Component
public class HostHolder {
    private ThreadLocal<User> users = new ThreadLocal<>();

    public void setUsers(User user){
        users.set(user);
    }

    public User getUser(){
        return users.get();
    }

    public void clear(){
        users.remove();
    }

}
public class CookieUtil {
    public static String getValue(HttpServletRequest request,String name) throws IllegalAccessException {
        if(request==null || name == null){
            throw new IllegalAccessException("参数为空!");
        }

        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for (Cookie cookie: cookies
                 ) {
                if(cookie.getName().equals(name)){
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从cookie中获取凭证
        String ticket = CookieUtil.getValue(request, "ticket");

        if(ticket != null){
            //查询凭证
            LoginTicket loginTicket = userService.selectByTicket(ticket);
            //检验凭证是否有效
            if(loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())){
                //根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                //在本次请求中持有用户
                hostHolder.setUsers(user);
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        User user = hostHolder.getUser();
        if(user != null && modelAndView != null){
            modelAndView.addObject("loginUser",user);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();
    }
}
@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");
    }
}
@Service
public class UserService implements CommunityConstant {

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    public LoginTicket selectByTicket(String ticket){
        return loginTicketMapper.selectByTicket(ticket);
    }
}

实现上传文件和修改密码功能

访问账号设置页面 

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping(path = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
        return "/site/setting";
    }
}

 上传头像和获取头像

#配置上传头像要到的文件路径
community.path.upload=C:/Users/86158/Pictures/headerImage
#配置Spring中文件上传的的上限
spring.servlet.multipart.maxFileSize=10MB
spring.servlet.multipart.maxRequestSize=20MB
@Controller
@RequestMapping("/user")
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Value("${community.path.upload}")
    private String uploadPath;

    @Value("${community.path.domain}")
    private String domain;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

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

    @RequestMapping(path = "/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 = CoumunityUtil.generateUUID() + suffix;
        //确定文件存放的路径
        File dest = new File(uploadPath + "/" + fileName);
        try {
            headerImage.transferTo(dest);
        } 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";
    }

    @RequestMapping(path = "/header/{fileName}",method = RequestMethod.GET)
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response){
        //服务器存储路径
        fileName = uploadPath + "/" + fileName;
        //文件后缀
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        //响应图片
        response.setContentType("image/" + suffix);

        try(
                FileInputStream fis = new FileInputStream(fileName);
                ServletOutputStream 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());
        }
    }
}
@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;

    public int updateHeader(int userId,String headerUrl){
        return userMapper.updateHeader(userId, headerUrl);
    }

}

修改密码 

业务层: 

@Service
public class UserService implements CommunityConstant {
    @Autowired
    private UserMapper userMapper;

    public Map<String,Object> updatePassword(User user,String prePassword,String curPassword,String repeatPassword){
        Map<String,Object> map = new HashMap<>();
        //判断旧密码是否为空
        if(StringUtils.isBlank(prePassword)){
            map.put("prePasswordMsg","原先密码不能为空");
            return map;
        }
        //判断新密码是否为空
        if(StringUtils.isBlank(curPassword)){
            map.put("curPasswordMsg","新密码不能为空");
            return map;
        }
        //判断新密码和旧密码是否相同
        if(prePassword.equals(curPassword)){
            map.put("curPasswordMsg","新密码不能和旧密码相同");
            return map;
        }
        //判断新密码和重复密码是否一致
        if(!curPassword.equals(repeatPassword)){
            map.put("repeatPasswordMsg","重复密码与新密码不一致");
            return map;
        }
        //执行更新密码
        String password = CoumunityUtil.md5(curPassword+user.getSalt());
        int result = userMapper.updatePassword(user.getId(), password);
        if(result != 1){
            map.put("curPasswordMsg","更新密码失败");
            return map;
        }
        return map;
    }
}

 控制层:

@Controller
@RequestMapping("/user")
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/modify/password",method = RequestMethod.POST)
    public String modifyPassword(Model model, String prePassword, String curPassword,String repeatPassword){
        User user = hostHolder.getUser();
        Map<String,Object> map = new HashMap<>();
        map = userService.updatePassword(user,prePassword,curPassword,repeatPassword);
        if(!map.isEmpty()){
            model.addAttribute("prePasswordMsg",map.get("prePasswordMsg"));
            model.addAttribute("curPasswordMsg",map.get("curPasswordMsg"));
            model.addAttribute("repeatPasswordMsg",map.get("repeatPasswordMsg"));
            return "/site/setting";
        }else{
            return "redirect:/login";
        }
    }
}

检查登录状态

上面常用的元注解的含义:

@Target注解用于描述注解的使用范围,也就是描述注解可以用在什么地方,它可以作用在——

@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包,用于记录java文件的package信息

@Retention注解用来描述注解的生命周期

@Retention 注解传入的是 RetentionPolicy 枚举,该枚举有三个常量,分别是 SOURCE、CLASS 和 RUNTIME

三者区别如下:

SOURCE 代表着注解仅保留在源级别中,编译器将Java文件编译成class文件时将之遗弃。
CLASS 代表着注解被保留在class文件中,JVM加载class文件时将之遗弃。
RUNTIME 代表着标记的注解会由JVM保留,因此运行时环境可以使用它。

@Document注解表示是否开启日志文件

@Inherited注解表示是否继承

因为在业务中有很多方法需要在用户未登录的情况下进行拦截,所以为了方便只需要在需要进行拦截的方法上面加上自定义的拦截注解即可,所以我们需要编制自定义的注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface 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 annotation = method.getAnnotation(LoginRequired.class);
            if(annotation != null && hostHolder.getUser() == null){
                response.sendRedirect(request.getContextPath() + "/login");
                return false;
            }
        }
        return true;
    }
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");
    }
}

过滤敏感词

前缀树算法 

根据前缀树的特点将敏感词挂载到树上:

定义三个指针,指针1指向根节点,指针2和指针3指向文本的开头。 

定义前缀树

@Component
public class SensitiveFilter {
    
    //根节点
    private TrieNode rootNode = new TrieNode();

    private class TrieNode{
        //关键词结束标识
        private boolean isKeywordEnd = false;
        
        //子节点(key是下级字符,value是下级节点)
        private Map<Character,TrieNode> subNodes = new HashMap<>();

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }
        
        //添加子节点
        public void addSubNode(Character c,TrieNode node){
            subNodes.put(c, node);
        }
        
        //获取子节点
        public TrieNode getSubNode(Character c){
            return subNodes.get(c);
        }
        
    }
}

 将敏感词挂到前缀树上:

    // 将一个敏感词添加到前缀树中
    private void addKeyword(String keyword) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);

            if (subNode == null) {
                // 初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }

            // 指向子节点,进入下一轮循环
            tempNode = subNode;

            // 设置结束标识
            if (i == keyword.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }

测试:

    public static void main(String[] args) {
        SensitiveFilter filter = new SensitiveFilter();
        //定义敏感词
        String work = "aabc";
        filter.addKeyword(work);
        System.out.println("Hello");
    }

过滤敏感词:

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


    /**
     * 过滤敏感词
     * @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 builder = new StringBuilder();

        while(position < text.length()){
            char c = text.charAt(position);

            //跳过符号
            if(isSymbol(c)){
                //若指针1处于根节点,将此符号计入结果,让指针2向下走一步
                if(tempNode == rootNode){
                    builder.append(c);
                    begin++;
                }
                //无论符号在开头或中间,指针3都向下走一步
                position++;
                continue;
            }
            //检查下级节点
            tempNode = tempNode.getSubNode(c);
            if(tempNode == null){
                //以begin开头的字符串不是敏感词
                builder.append(text.charAt(begin));
                //进入下一个位置
                position = ++begin;
                //重新指向根节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd) {
                //发现敏感词,将begin~position字符串换掉
                builder.append(REPLACEMENT);
                //进入下一个位置
                begin = ++position;
                //重新指向根节点
                tempNode = rootNode;
            }else {
                //检查下一个字符
                position++;
            }
        }
        //将最后一批字符计入结果
        builder.append(text.substring(begin));
        return builder.toString();
    }

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

测试:

过滤算法执行的流程 :

编写敏感词文件,并将该文件在SensitiveFilter初始化的时候加入到前缀树中:

@Component
public class SensitiveFilter {

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

    @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 (Exception e) {
            logger.error("加载敏感词文件失败:" + e.getMessage());
        }

    }

}

 测试:

发布帖子

发布帖子需要用到动态变化的页面,这里用异步处理,使用AJAX来实现前后端之间数据的交互。

示例使用jQuery来发送AJAX请求 

引入依赖fastjson

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.26</version>
</dependency>

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

    public static void main(String[] args) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("name","zhangsan");
        map.put("age",24);
        System.out.println(getJsonString(0, "ok", map));
    }
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>AJAX</title>
</head>
<body>
    <p>
        <input type="button" value="发送" onclick="send();">
    </p>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
    <script>
        function send(){
            $.post(
                "/community/alpha/ajax",
                {"name":"张三","age":23},
                function (data){
                    console.log(typeof(data));
                    console.log(data);

                    data = $.parseJSON(data);
                    console.log(data);
                    console.log(data.code);
                    console.log(data.msg);
                }
            );
        }
    </script>
</body>
</html>
@Controller
@RequestMapping("/alpha")
public class AlphaController {
    @RequestMapping(path = "/ajax",method = RequestMethod.POST)
    @ResponseBody
    public String getJsonString(String name,int age){
        System.out.println(name);
        System.out.println(age);
        return CommunityUtil.getJsonString(0,"操作成功");
    }
}

 

采用AJAX实现发布帖子功能 

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

 数据访问层:

@Mapper
public interface DiscussPostMapper {
    int insertDiscussPost(DiscussPost discussPost);
}

业务层:

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Autowired
    private SensitiveFilter sensitiveFilter;
    public int addDiscussPost(DiscussPost post){
        //转义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);
    }

}

视图层:

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "add",method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title,String content){
        if(StringUtils.isBlank(title) || StringUtils.isBlank(content)){
            return CommunityUtil.getJsonString(-1,"标题或者正文不能为空");
        }
        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,"发布成功");
    }
}

js代码:

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

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

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

	//发送异步请求(POST)
	$.post(
		CONTEXT_PATH + "/discuss/add",
		{"title":title,"content":content},
		function (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);
		}
	);
}

帖子详情

sql配置文件:

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

 数据访问层:

@Mapper
public interface DiscussPostMapper {
    DiscussPost selectDiscussPost(int id);
}

业务层:

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    public DiscussPost selectDiscussPost(int id){
        return discussPostMapper.selectDiscussPost(id);
    }

}

视图层:

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
    public String selectDiscussPostById(Model model, @PathVariable("discussPostId") int discussPostId){
        DiscussPost discussPost = discussPostService.selectDiscussPost(discussPostId);
        model.addAttribute("post",discussPost);
        User user = userService.findUserById(discussPost.getUserId());
        System.out.println(user);
        model.addAttribute("user",user);
        return "/site/discuss-detail";
    }
}

事务管理

         

关于Sping事务和事务传播机制可以看 Spring事务和事务传播机制_咸鱼吐泡泡的博客-CSDN博客

事务管理的测试案例:

申明式事务——

@Service
public class AlphaService {

    @Autowired
    private AlphaDao alphaDao;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public Object save1(){
        //新增用户
        User user = new User();
        user.setUsername("韩梅梅");
        user.setSalt(CommunityUtil.generateUUID().substring(0,5));
        user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
        user.setEmail("test@qq.com");
        user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
        user.setCreateTime(new Date());
        userMapper.insertUser(user);

        //新增帖子
        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle("Hello");
        post.setContent("我想离开浪浪山");
        post.setCreateTime(new Date());
        discussPostMapper.insertDiscussPost(post);

        Integer.valueOf("abc");
        return "ok";
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TranscationTest {

    @Autowired
    private AlphaService alphaService;

    @Test
    public void transcationTest(){
        Object o = alphaService.save1();
        System.out.println(o);
    }
}

 

事务发生了回滚

编程式事务:

@Service
public class AlphaService {

    @Autowired
    private AlphaDao alphaDao;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    public Object save2(){
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        return transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                //新增用户
                User user = new User();
                user.setUsername("韩梅梅");
                user.setSalt(CommunityUtil.generateUUID().substring(0,5));
                user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
                user.setEmail("test@qq.com");
                user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
                user.setCreateTime(new Date());
                userMapper.insertUser(user);

                //新增帖子
                DiscussPost post = new DiscussPost();
                post.setUserId(user.getId());
                post.setTitle("Hello");
                post.setContent("我想离开浪浪山");
                post.setCreateTime(new Date());
                discussPostMapper.insertDiscussPost(post);

                Integer.valueOf("abc");
                return "ok";
            }
        });
    }
}

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

    @Autowired
    private AlphaService alphaService;

    @Test
    public void transcationTest2(){
        Object o = alphaService.save2();
        System.out.println(o);
    }
}

显示评论

package com.nowcoder.community.entity;


import java.util.Date;

public class Comment {
    private int id;
    private int userId;
    private int entityType;
    private int entityId;
    private int targetId;
    private String content;
    private int status;
    private Date createTime;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getEntityType() {
        return entityType;
    }

    public void setEntityType(int entityType) {
        this.entityType = entityType;
    }

    public int getEntityId() {
        return entityId;
    }

    public void setEntityId(int entityId) {
        this.entityId = entityId;
    }

    public int getTargetId() {
        return targetId;
    }

    public void setTargetId(int targetId) {
        this.targetId = targetId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", userId=" + userId +
                ", entityType=" + entityType +
                ", entityId=" + entityId +
                ", targetId=" + targetId +
                ", content='" + content + '\'' +
                ", status=" + status +
                ", createTime=" + createTime +
                '}';
    }
}
<?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.nowcoder.community.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>
@Mapper
public interface CommentMapper {
    /**
     *查询帖子下面的所有评论
     * @param entityType    评论的类型(对帖子进行评论、对评论进行回复)
     * @param entityId      是那一条帖子或者评论被评论/回复
     * @param offset        分页的起始行
     * @param limit         一页允许有多少条数据
     * @return              返回评论内容的集合(评论人——userId、评论的类型——entityType、评论的目标(帖子/评论)——entityId、在评论下面回复谁——targetId、回复的内容——content、评论的状态——status、评论的创建时间——createTime)
     */
    List<Comment> selectCommentsByEntity(int entityType,int entityId,int offset,int limit);


    /**
     * 查询帖子下面评论的数量
     * @param entityType    评论的类型(对帖子进行评论、对评论进行回复)
     * @param entityId      是那一条帖子或者评论被评论/回复
     * @return
     */
    int selectCountByEntity(int entityType,int entityId);
}
import java.util.List;

@Service
public class CommentService {

    @Autowired
    private CommentMapper commentMapper;

    public List<Comment> selectCommentsByEntity(int entityType,int entityId,int offset,int limit){
        return commentMapper.selectCommentsByEntity(entityType,entityId,offset,limit);
    }

    public int findCommentCount(int entityType,int entityId){
        return commentMapper.selectCountByEntity(entityType,entityId);
    }
}
@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @Autowired
    private CommentService commentService;


    @RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
    public String selectDiscussPostById(Model model, @PathVariable("discussPostId") int discussPostId,Page page){
        //帖子
        DiscussPost discussPost = discussPostService.selectDiscussPost(discussPostId);
        model.addAttribute("post",discussPost);
        //作者
        User user = userService.findUserById(discussPost.getUserId());
        System.out.println(user);
        model.addAttribute("user",user);
        //评论分页信息
        page.setLimit(5);
        page.setPath("/discuss/detail/" + discussPostId);
        page.setRows(discussPost.getCommentCount());

        //评论列表
        List<Comment> commentList = commentService.selectCommentsByEntity(
                ENTITY_TYPE_POST,discussPost.getId(), page.getOffset(), page.getLimit());
        //要返回给前端的评论视图列表
        List<Map<String,Object>> commentVoList = new ArrayList<>();
        if(commentList != null){
            for (Comment comment : commentList) {
                //评论的视图
                Map<String,Object> commentVo = new HashMap<>();
                //评论
                commentVo.put("comment",comment);
                //作者
                commentVo.put("user",userService.findUserById(comment.getUserId()));

                //回复列表
                List<Comment> replyList = commentService.selectCommentsByEntity(
                        ENTITY_TYPE_COMMENT,comment.getId(),0,Integer.MAX_VALUE
                );
                //回复视图列表
                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.put("replys",replyVoList);
                //回复数量
                int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT,comment.getId());
                commentVo.put("replyCount",replyCount);
                commentVoList.add(commentVo);
            }
        }
        model.addAttribute("comments",commentVoList);
        return "/site/discuss-detail";
    }
}

添加评论

因为添加评论这一个动作有两次DML操作,所以要用到事务。

    <update id="updateDiscussPostCommentCount" parameterType="DiscussPost">
        update discuss_post set comment_count = #{commentCount} where id = #{id}
    </update>
@Mapper
public interface DiscussPostMapper {
    int updateDiscussPostCommentCount(int id,int commentCount);

}
@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    public int updateDiscussPostCommentCount(int id,int comentCount){
        return discussPostMapper.updateDiscussPostCommentCount(id,comentCount);
    }

}

    <insert id="insertComment" parameterType="Comment">
        insert into comment(<include refid="insertFields"></include>)
        values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime});
    </insert>
@Mapper
public interface CommentMapper {
    int insertComment(Comment comment);
}

@Service
public class CommentService implements CommunityConstant {

    @Autowired
    private CommentMapper commentMapper;

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Autowired
    private SensitiveFilter sensitiveFilter;


    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public int insertComment(Comment comment) throws IllegalAccessException {
        if(comment == null){
            throw new IllegalAccessException("参数不能为空");
        }
        //添加评论
        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());
            discussPostMapper.updateDiscussPostCommentCount(comment.getEntityId(),count);
        }
        return rows;
    }
}
import java.util.Date;

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

    @Autowired
    private CommentService commentService;

    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(path = "/add/{discussPostId}",method = RequestMethod.POST)
    public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) throws IllegalAccessException {
        comment.setUserId(hostHolder.getUser().getId());
        comment.setStatus(0);
        comment.setCreateTime(new Date());
        commentService.insertComment(comment);
        return "redirect:/discuss/detail/" + discussPostId;
    }
}

注意这里的重定向是定向到后端的selectDiscussPostById方法,这样才会使前端页面刷新出新数据。

私信消息

<?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.nowcoder.community.dao.MessageMapper">
    <sql id="selectFields">
        id,from_id,to_id,conversation_id,content,status,create_time
    </sql>

    <sql id="insertFields">
        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) as 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
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);

}
@Service
public class MessageService {
    @Autowired
    private MessageMapper messageMapper;

    @Autowired
    private SensitiveFilter sensitiveFilter;

    public List<Message> selectConversations(int userId,int offset,int limit){
        return messageMapper.selectConversations(userId, offset, limit);
    }

    public int selectConversationCount(int userId){
        return messageMapper.selectConversationCount(userId);
    }

    public List<Message> selectLetters(String conversationId,int offset,int limit){
        return messageMapper.selectLetters(conversationId, offset, limit);
    }

    public int selectLetterCount(String conversationId){
        return messageMapper.selectLetterCount(conversationId);
    }

    public int selectLetterUnreadCount(int userId,String conversationId){
        return messageMapper.selectLetterUnreadCount(userId, conversationId);
    }
}
@Controller
public class MessageController {
    @Autowired
    private MessageService messageService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/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.selectConversationCount(user.getId()));

        //会话列表
        List<Message> conversationList = messageService.selectConversations(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("letterCount",messageService.selectLetterCount(message.getConversationId()));
                map.put("unreadCount",messageService.selectLetterUnreadCount(user.getId(), 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.selectLetterUnreadCount(user.getId(), null);
        model.addAttribute("letterUnreadCount",letterUnreadCount);
        return "/site/letter";
    }

    @RequestMapping(path = "/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.selectLetterCount(conversationId));

        //私信列表
        List<Message> letterList = messageService.selectLetters(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";
    }

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

发送私信

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

    <update id="updateUnreadLetter" parameterType="Message">
        update message set status = #{status}
        where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </update>
    //插入一条私信
    int insertLetter(Message message);

    //更新未读私信的状态
    int updateUnreadLetter(List<Integer> ids,int status);
    public int insertLetter(Message message){
        message.setContent(HtmlUtils.htmlEscape(message.getContent()));
        message.setContent(sensitiveFilter.filter(message.getContent()));
        return messageMapper.insertLetter(message);
    }

    public int updateUnreadLetter(List<Integer> ids,int status){
        return messageMapper.updateUnreadLetter(ids, status);
    }
    @RequestMapping(path = "/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.selectLetterCount(conversationId));

        //私信列表
        List<Message> letterList = messageService.selectLetters(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));

        //设置已读
        List<Integer> ids = getLetterIds(letterList);
        if(!ids.isEmpty()){
            messageService.updateUnreadLetter(ids,1);
        }
        return "/site/letter-detail";
    }

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

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

    @RequestMapping(path = "/letter/send",method = RequestMethod.POST)
    @ResponseBody
    public String sendLetter(String toName,String content){
        User target = userService.findUserByName(toName);
        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.setStatus(0);
        message.setCreateTime(new Date());
        messageService.insertLetter(message);
        return CommunityUtil.getJsonString(0);
    }

统一异常处理

在Spring或者SpringBoot中,只需要将异常的静态资源放在templates目录下面,当程序的某处发生异常Spring就会返回异常资源:

在站内访问不存在的路径时: 

 当服务器有问题时:

但是这种笼统的处理方式当访问出错时我们并不知道服务器到底是哪里出错了,因此我们一般不采用这种方式。

而是采用下面这种方式:

@ControllerAdvice这个注解——控制器的通知,他会对Controller做全局的配置,当无论那个Controller发生错误就会做统一的处理,详细情况:@ControllerAdvice 用法_探索er的博客-CSDN博客

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

}
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
    @ExceptionHandler({Exception.class})
    private void handerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常: " + e.getMessage());
        for (StackTraceElement element : e.getStackTrace()) {
            logger.error(element.toString());
        }
        //判断普通请求还是异步请求
        String xRequestedWith = request.getHeader("x-requested-with");
        if("XMLHttpRequest".equals(xRequestedWith)){//异步请求
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJsonString(1,"服务器异常!"));
        }else{
            response.sendRedirect(request.getContextPath() + "/error");
        }
    }
}

 上面的@ControllerAdvice注解他会扫描全部的Bean,会浪费一点时间,而我们只需要添加annotations = Controller.class它就只会扫描带有带有@Controller方法。@ExceptionHandle

r括号里面表示异常的类型,而Exception他是所有异常的父类。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Kafka是一个高性能的分布式消息队列系统,可以实现高吞吐量、低延迟的消息传递。它支持点对点和发布-订阅两种消息传递模式。在仿牛客项目中使用Kafka可以实现消息的异步处理和分布式架构。 使用Kafka的第一步是创建一个主题(topic),主题既是消息的类别,也是消息在Kafka中的存储位置。可以使用命令行工具kafka-topics.bat来创建主题。例如,可以使用以下命令来创建一个名为test的主题: bin\windows\kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test 上述命令中,--bootstrap-server参数指定了Kafka服务器的地址和端口,--replication-factor参数指定了主题的副本数,--partitions参数指定了主题的分区数。创建主题后,可以向主题中发送消息,并由消费者进行消费。 要列出已经存在的主题,可以使用以下命令: kafka-topics.bat --list --bootstrap-server localhost:9092 需要注意的是,以上命令中的localhost:9092是Kafka服务器的地址和端口,根据实际情况进行修改。 总结起来,在仿牛客项目中使用Kafka,首先需要创建一个主题,然后可以使用相关命令行工具进行消息的发送和消费。这样可以实现消息的异步处理和分布式架构。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [仿牛客论坛项目学习记录——5 Kafka 构建TB级异步消息系统](https://blog.csdn.net/dadayangpei/article/details/127173098)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咸鱼吐泡泡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值