赞
踩
代码仓库:https://gitee.com/qiuyusy/community
@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(classes = CommunityApplication.class) class CommunityApplicationTests implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Test public void testApplicationContext() { System.out.println(applicationContext); String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); } } }
实际开发中没必要这样,直接@Autowired自动装配进来就行
比如
@SpringBootTest
class CommunityApplicationTests {
@Autowired
SimpleDateFormat simpleDateFormat;
@Test
void contextLoads() {
System.out.println(simpleDateFormat.format(new Date()));
}
}
如果使用接口获取Bean,这个接口下有多个实现类就会报错
AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class);
可以使用@Primary指定使用哪个是实现类
但是如果我某些地方还是想使用Hibernate的实现类怎么办,可以定义name来解决
@Repository("alphaHibernate")
public class AlphaDaoHibernateImpl implements AlphaDao {
@Override
public String select() {
return "Hibernate";
}
}
AlphaDao alphaDao = applicationContext.getBean("alphaHibernate", AlphaDao.class);
AlphaDao alphaDao = (AlphaDao) applicationContext.getBean("alphaHibernate");
实际开发中没必要用@Primary 直接 @Qualifier(“alphaHibernate”)指定就行了
class CommunityApplicationTests {
@Autowired
@Qualifier("alphaHibernate")
AlphaDao alphaDao;
@Test
public void test(){
System.out.println(alphaDao.select());
}
}
@Service public class AlphaService { public AlphaService() { System.out.println("执行构造方法"); } @PostConstruct public void init(){ System.out.println("初始化..."); } @PreDestroy public void destroy(){ System.out.println("销毁"); } }
@PostConstruct 在构造器之后调用方法
@PreDestroy 在对象销毁前调用方法
使用配置类来注入Bean
package com.qiuyu.config;
@Configuration
public class AlphaConfig {
@Bean
public SimpleDateFormat simpleDateFormat(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overview#http_%E6%B5%81
当客户端想要和服务端进行信息交互时(服务端是指最终服务器,或者是一个中间代理),过程表现为下面几步:
打开一个 TCP 连接
发送一个 HTTP 报文
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
读取服务端返回的报文信息:
HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!DOCTYPE html... (here comes the 29769 bytes of the requested web page)
关闭连接或者为后续请求重用连接。
这里的Front Controller 前端控制器其实就是DispatcherServlet
发送ModelAndView给模板
//第一种ModelAndView @RequestMapping(value = "/teacher", method = RequestMethod.GET) public ModelAndView getTeacher(){ ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("name", "qiuyu"); modelAndView.addObject("age", 18); modelAndView.setViewName("demo/teacher"); return modelAndView; } //第二种Model @RequestMapping(value = "/teacher", method = RequestMethod.GET) public String getTeacher(Model model){ model.addAttribute("name", "qiuyu"); model.addAttribute("age", 19); return "demo/teacher"; }
模板读取数据然后写入html,记得放在templates下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${name}"> </p>
<p th:text="${age}"> </p>
</body>
</html>
主要目的是为了加入路径,让前端的分页更好的复用
/**
* 我的分页组件
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyPage<T> extends Page<T> {
/**
* 分页跳转的路径
*/
protected String path;
}
直接Mybatis-Plus生成
@Mapper
public interface DiscussPostMapper extends BaseMapper<DiscussPost> {
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
package com.qiuyu.service; @Service public class DiscussPostService { @Autowired private DiscussPostMapper discussPostMapper; /** * 查询不是被拉黑的帖子,并且userId不为0按照type排序 * @param userId * @Param page * @return */ public IPage<DiscussPost> findDiscussPosts(int userId, IPage<DiscussPost> page) { LambdaQueryWrapper<DiscussPost> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper .ne(DiscussPost::getStatus, 2) .eq(userId != 0, DiscussPost::getUserId, userId) .orderByDesc(DiscussPost::getType, DiscussPost::getCreateTime); discussPostMapper.selectPage(page, queryWrapper); return page; } /** * userId=0查所有;userId!=0查个人发帖数 * * @param userId * @return */ public int findDiscussPostRows(int userId) { LambdaQueryWrapper<DiscussPost> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper .ne(DiscussPost::getStatus, 2) .eq(userId != 0, DiscussPost::getUserId, userId); int nums = discussPostMapper.selectCount(queryWrapper); return nums; } }
package com.qiuyu.service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User findUserById(String id) {
return userMapper.selectById(Integer.parseInt(id));
}
}
这里查询贴子,找到的只是userid,所以需要用userid找出user
采用Map的形式是为了之后Redis更方便
@GetMapping("/index") public String getIndexPage(Model model, MyPage<DiscussPost> page) { page.setSize(10); page.setPath("/index"); //查询到分页的结果 page = (MyPage<DiscussPost>) discussPostService.findDiscussPosts(0, page); List<DiscussPost> list = page.getRecords(); //因为这里查出来的是userid,而不是user对象,所以需要重新查出user List<Map<String, Object>> discussPorts = new ArrayList<>(); if (list != null) { for (DiscussPost post : list) { Map<String, Object> map = new HashMap<>(15); map.put("post", post); User user = userService.findUserById(post.getUserId()); map.put("user", user); discussPorts.add(map); } } model.addAttribute("discussPorts", discussPorts); model.addAttribute("page", page); return "/index"; }
按理说MyPage会自动放入model中,但是这里得手动加入,否则前端读取不到,不知道为啥
@表示前面加个项目路径(/community
)
先导入thymeleaf<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="css/global.css" />
<title>牛客网-首页</title>
注意,因为把网页划分到了static和templates中,相对路径可能会找不到,可以加th
解决,意思为让其到static下找资源
下面的js也要这么处理
<link rel="stylesheet" th:href="@{/css/global.css}" />
<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/index.js}"></script>
<li th:each="map:${discussPorts}">
th:each
用于循环输出数据${discussPorts}
discussPorts为Model传过来的数据名称map
为在这里使用的属性名<li th:each="i:${#numbers.sequence(page.current-2,page.current+2)}">
<a th:text="${i}" th:if="${i} > 0 and ${i} <= ${page.pages} "></a>
</li>
#numbers.sequence
按照给定的开始和结束的数,从开始循环到结束<p th:text="mapStat.count"></p
mapStat.count
属性名+Stat.count获取当前循环到第几个<img th:src="${map.user.headerUrl}" >
${map.user.headerUrl}
拿到map中的user属性的headerUrl属性<a href="#" th:utext="${map.post.title}">
${map.post.title}
拿到map中的post属性的title属性<span th:if="${map.post.type==1}">置顶</span>
map.post.type
等于 1 才显示置顶使用dates工具格式化
<b th:utext="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">
<p th:text="${#lists.size(discussPorts)}"></p>
<a th:href="@{/community/index(currentPage=1,pageSize=10)}">首页</a>
<a th:href="@{${path}(currentPage=${page.getPages()},pageSize=10)}">末页</a>
先用| |
把class内括起来 然后写判断
<li th:class="|page-item ${page.current==1?'disabled':''}|">
设置默认值
<input type="text" th:value="${user!=null ? user.username : ''}"
id="username" name="username" placeholder="请输入您的账号!" required>
<!-- 分页 --> <nav class="mt-5" th:if="${page.pages > 1}" 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.current-2,page.current+2)}"> <a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}" th:if="${i} > 0 and ${i} <= ${page.pages} "></a> </li> <li class="page-item" th:class="|page-item ${page.current==page.pages?'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.pages})}">末页</a> </li> </ul> </nav>
trace>debug>info>warn>error
level下写的是包,填写最低显示级别
#logger
logging:
level:
com.qiuyu: warn
然后创建一个Logger就行(注意是 org.slf4j.Logger )
@SpringBootTest
@RunWith(SpringRunner.class)
public class LoggerTest {
private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class);
@Test
public void testLogger(){
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");;
}
}
简单
logging:
level:
com.qiuyu: debug
file:
name: community.log
复杂
使用logback-spring.xml配置
邮箱需要开启SMTP协议
导入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置
spring:
# 邮箱
mail:
#配置邮件消息
host: smtp.qq.com
port: 465
#发送邮件者信箱(也就是你申请POP3/SMTP服务的QQ号)
username: ***@qq.com
#申请PO3/SMTP服务时,给我们的邮箱的授权码
password: *****
default-encoding: UTF-8
protocol: smtp
properties:
mail.smtp.ssl.enable: true
写一个工具类用于发邮件
package com.qiuyu.utils; @Component public class MailClient { private static final Logger logger = LoggerFactory.getLogger(MailClient.class); @Autowired private JavaMailSender mailSender; /** * 从yml中读取发件人 */ @Value("${spring.mail.username}") private String from; public void sendMail(String to, String subject, String content) { try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message); //设置收发件人 helper.setFrom(from); helper.setTo(to); //设置邮件 helper.setSubject(subject); helper.setText(content,true); //true表示支持html格式 //发送 mailSender.send(helper.getMimeMessage()); } catch (MessagingException e) { logger.error("发送邮件失败" + e.getMessage()); } finally { } } } @Test public void testSendMail() { String to = "****@qq.com"; String subject = "测试邮件"; String content = "测试邮件内容"; mailClient.sendMail(to, subject, content); }
写一个模板
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮箱示例</title>
</head>
<body>
<p>欢迎你,<span style="color: red" th:text="${username}"></span></p>
</body>
</html>
发送邮件
package com.qiuyu.utils; 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.test.context.junit4.SpringRunner; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; @SpringBootTest @RunWith(SpringRunner.class) public class MailClientTest { @Autowired private MailClient mailClient; @Autowired private TemplateEngine templateEngine; @Test public void testSendHtmlMail() { String to = "2448567284@qq.com"; String subject = "测试邮件"; //创建数据 Context context = new Context(); context.setVariable("username", "qiuyu"); //根据模板,放入数据 String content = templateEngine.process("/mail/demo", context); System.out.println(content); //发送 mailClient.sendMail(to, subject, content); } }
@Controller
public class LoginController {
@GetMapping("/register")
public String getRegisterPage() {
return "/site/register";
}
}
index.html
<header class="bg-dark sticky-top" th:fragment="header">
th:fragment="header"
被复用的部分,取名header
register.html
<header class="bg-dark sticky-top" th:replace="index::header"></header>
th:replace="index::header"
复用index的header导入一个字符串处理工具类依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
配置一下域名,让邮箱访问(key可以自定义)
# community
community:
path:
domain: http://localhost:80
写一个工具类,包括获得随机字符串和md5加密
密码在存入数据库时,需要进行md5加密
但是如果简单密码经过md5加密后,也可能会被黑客撞库攻击所以先将密码进行加盐(salt)后再进行md5加密
package com.qiuyu.utils; import org.apache.commons.lang3.StringUtils; import org.springframework.util.DigestUtils; import java.util.UUID; public class CommunityUtil { /* * 生成随机字符串 * 用于邮件激活码,salt5位随机数加密 **/ public static String generateUUID(){ return UUID.randomUUID().toString().replaceAll("-",""); } /* MD5加密 * hello-->abc123def456 * hello + 3e4a8-->abc123def456abc */ public static String md5(String key){ //检查时候为null 空 空格 if (StringUtils.isBlank(key)){ return null; } //MD5加密方法 return DigestUtils.md5DigestAsHex(key.getBytes()); //参数是bytes型 } }
还有常量接口(实现接口使用)
package com.qiuyu.utils; /** * @author QiuYuSY * @create 2023-01-17 22:06 * 一些常量 */ public interface CommunityConstant { /* 以下用于注册功能 */ /** 激活成功*/ int ACTIVATION_SUCCESS=0; /** 重复激活 */ int ACTIVATION_REPEAT=1; /** 激活失败 */ int ACTIVATION_FAILURE=2; /* 以下用于登录功能* / /** * 默认状态的登录凭证的超时时间 */ int DEFAULT_EXPIRED_SECONDS=3600*12; /** * 记住状态的登录凭证超时时间 */ int REMEMBER_EXPIRED_SECONDS=3600*24*7; }
输入合法性验证
注册账户
发送邮件
public Map<String,Object> register(User user){ Map<String,Object> map = new HashMap<>(); //空值处理 if (user == null) { throw new IllegalArgumentException("参数不能为空"); } if(StringUtils.isBlank(user.getUsername())){ map.put("usernameMsg", "账号不能为空"); return map; } if(StringUtils.isBlank(user.getPassword())){ map.put("passwordMsg", "密码不能为空"); return map; } if(StringUtils.isBlank(user.getEmail())){ map.put("emailMsg", "邮箱不能为空"); return map; } //判断账号是否被注册 Integer integer = userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername())); if(integer > 0){ map.put("usernameMsg", "该账号已被注册"); return map; } //判断邮箱是否被注册 integer = userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getEmail, user.getEmail())); if(integer > 0){ map.put("emailMsg", "该邮箱已被注册"); return map; } //给用户加盐 user.setSalt(CommunityUtil.generateUUID().substring(0,5)); //加密 user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); //初始化其他数据 user.setType(0); user.setStatus(0); user.setActivationCode(CommunityUtil.generateUUID()); user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); user.setCreateTime(new Date()); //注册用户 userMapper.insert(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); //map为空则注册成功 return map; }
/** * 激活账号 * @param userId * @param activationCode * @return */ public int activate(int userId, String activationCode) { //根据userid获取用户信息 User user = userMapper.selectById(userId); if(user.getStatus() == 1){ //已经激活,则返回重复 return ACTIVATION_REPEAT; } else if (user.getActivationCode() .equals(activationCode)) { //如果未激活,判断激活码是否相等 //激活账号 user.setStatus(1); userMapper.updateById(user); return ACTIVATION_SUCCESS; } else { //不相等 return ACTIVATION_FAILURE; } }
package com.qiuyu.controller; @Controller public class LoginController { @Autowired private UserService userService; /** * 跳转到请求页面 * @return */ @GetMapping("/register") public String getRegisterPage() { return "/site/register"; } /** * 注册账号,发送邮箱 */ @PostMapping("/register") public String register(Model model, User user) { Map<String, Object> map = userService.register(user); if(map == null || map.isEmpty()){ //注册成功,跳转到中转页面 model.addAttribute("msg","注册成功,我们已经向您的邮件发送了一封激活邮件,请尽快激活!"); model.addAttribute("target","/index"); return "/site/operate-result"; }else{ //注册失败,重新注册 model.addAttribute("usernameMsg",map.get("usernameMsg")); model.addAttribute("passwordMsg",map.get("passwordMsg")); model.addAttribute("emailMsg",map.get("emailMsg")); return "/site/register"; } } }
/** * 激活邮箱 http://localhost:8080/community/activation/101/code 激活链接 * @param model * @param userId * @param code * @return */ @GetMapping("/activation/{userId}/{code}") public String activate(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) { int result = userService.activate(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"; }
密码之类隐私数据用Cookie放浏览器不安全
@GetMapping("/cookie/set") @ResponseBody public String setCookie(HttpServletResponse response) { //创建Cookie Cookie cookie = new Cookie("code", CommunityUtil.generateUUID()); //设置Cookie生效范围 cookie.setPath("/community"); //设置cookie有效时间(s) cookie.setMaxAge(60 * 10); //发送Cookie response.addCookie(cookie); return "setCookie"; } @GetMapping("/cookie/get") @ResponseBody public String getCookie(@CookieValue("code") String code) { return code; }
服务器把sessionId用cookie给浏览器,浏览器只存了sessionId
缺点是耗费内存
Set-Cookie: JSESSIONID=71B1E0DDFA9BD595C5E7F584AD56E7F6; Path=/community; HttpOnly
@GetMapping("/session/set") @ResponseBody public String setSession(HttpSession session) { session.setAttribute("id",1); session.setAttribute("name","Test"); session.setAttribute("pwd","ASDASDDADASD"); return "setSession"; } @GetMapping("/session/get") @ResponseBody public String getSession(HttpSession session) { System.out.println(session.getAttribute("id")); System.out.println(session.getAttribute("name")); System.out.println(session.getAttribute("pwd")); return "getSession"; }
为什么Session在分布式情况下尽量少使用
因为负载均衡无法保证同个用户的多次请求都能到同一台服务器,而session只在第一次请求的服务器中
解决方案
参考网站 :http://code.google.com/archive/p/kaptcha/
注意:
1.Producer是Kaptcha的核心接口
2.DefaultKaptcha是Kaptcha核心接口的默认实现类
3.Spring Boot没有为Kaptcha提供自动配置
导入
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
配置
package com.qiuyu.config; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; @Configuration public class KaptchaConfig { @Bean public Producer KaptchaProducer(){ /** * 手动创建properties.xml配置文件对象* * 设置验证码图片的样式,大小,高度,边框,字体等 */ Properties properties=new Properties(); properties.setProperty("kaptcha.border", "yes"); properties.setProperty("kaptcha.border.color", "105,179,90"); properties.setProperty("kaptcha.textproducer.font.color", "black"); properties.setProperty("kaptcha.image.width", "110"); //宽度 properties.setProperty("kaptcha.image.height", "40"); //高度 properties.setProperty("kaptcha.textproducer.font.size", "32"); //字号 properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); properties.setProperty("kaptcha.textproducer.char.length", "4"); //几个字符 properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); 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; } }
使用(注意生成的文本要放入session,等待验证用户的输入)
@GetMapping("/kaptcha") 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 { OutputStream os = response.getOutputStream(); ImageIO.write(image,"png",os); os.flush(); } catch (IOException e) { logger.error("响应验证码失败:"+e.getMessage()); } }
<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" />
<a href="javascript:refresh_kaptcha();">刷新验证码</a>
有些浏览器认为图片为静态资源,地址没变,就不刷新,带个参数可以解决
<script>
function refresh_kaptcha(){
//用?带个参数欺骗浏览器,让其认为是个新路径
var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
$("#kaptcha").attr("src", path);
}
</script>
登录后需要使用cookie或session进行登录凭证的验证,但是上面说到了这两种方案的缺点
这里使用把凭证存入数据库的方式,先存入mysql,后续转redis
package com.qiuyu.bean;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginTicket {
private Integer id;
private Integer userId;
private String ticket;
private Integer status;
private Date expired;
}
MyBatis-Plus生成
package com.qiuyu.service; @Service public class LoginService { @Autowired private LoginTicketMapper loginTicketMapper; @Autowired private UserMapper userMapper; /** * 登录 * @param username * @param password * @param expiredSeconds * @return */ public Map<String,Object> login(String username, String password, int expiredSeconds){ HashMap<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.selectOne(new LambdaQueryWrapper<User>() .eq(User::getUsername, username)); if(user == null){ map.put("usernameMsg","该账号不存在"); return map; } //验证激活状态 if(user.getStatus() == 0){ map.put("usernameMsg","该账号未激活"); return map; } //验证密码(先加密再对比) String pwdMd5 = CommunityUtil.md5(password + user.getSalt()); if(!pwdMd5.equals(user.getPassword())){ map.put("passwordMsg","密码错误"); return map; } //生成登录凭证(相当于记住我这个功能==session) LoginTicket ticket = new LoginTicket(); ticket.setUserId(user.getId()); ticket.setTicket(CommunityUtil.generateUUID()); ticket.setStatus(0); //有效 //当前时间的毫秒数+过期时间毫秒数 ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000)); Date date = new Date(); loginTicketMapper.insert(ticket); map.put("ticket",ticket.getTicket()); //map中能拿到ticket说明登录成功了 return map; } }
/** * 登录功能 * @param username * @param password * @param code 验证码 * @param rememberme 是否勾选记住我 * @param model * @param session 用于获取kaptcha验证码 * @param response 用于浏览器接受cookie * @return */ @PostMapping(path = "/login") 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"; } /* * 1.验证用户名和密码(重点) * 2.传入浏览器cookie=ticket */ int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS; Map<String, Object> map = loginService.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"; } }
<input type="text" name="username"
th:value="${param.username}"
id="username" placeholder="请输入您的账号!" required>
<input type="checkbox" name="rememberme" id="remember-me"
th:checked="${param.rememberme}">
param
<input type="password" name="password"
th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
th:value="${param.password}"
id="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
is-invalid
样式显示提示/**
* 登出
* @param ticket 登录凭证
*/
public void logout(String ticket){
LoginTicket loginTicket = new LoginTicket();
loginTicket.setStatus(1);
loginTicketMapper.update(loginTicket,
new LambdaUpdateWrapper<LoginTicket>().eq(LoginTicket::getTicket,ticket));
}
/**
* 退出登录功能
* @CookieValue()注解:将浏览器中的Cookie值传给参数
*/
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
return "redirect:/login";//重定向
}
@CookieValue
将浏览器中的Cookie值传给参数@Component public class AlphaInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.debug("preHandle"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.debug("postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { logger.debug("afterCompletion"); } }
package com.qiuyu.config; import com.qiuyu.controller.interceptor.AlphaInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @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"); //只拦截部分请求 } }
Request获取Cookie工具类,获取凭证ticket多线程工具类
package com.qiuyu.utils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; public class CookieUtil { /** * 从request中获取指定cookie对象 * @param request * @param name * @return */ 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; } }
注意:
ThreadLocal采用线程隔离的方式存放数据,可以避免多线程之间出现数据访问冲突。
ThreadLocal提供set方法,能够以当前线程为key存放数据。get方法,能够以当前线程为key获取数据。
ThreadLocal提供remove方法,能够以当前线程为key删除数据。
因为用户登录后,需要把用户信息放入内存之中,而web时多线程的环境,每个用户都会有一个线程
为了避免线程之间干扰,需要采用ThreadLocal进行线程隔离
package com.qiuyu.utils; import com.qiuyu.bean.User; import org.springframework.stereotype.Component; /** * 持有用户信息,代替session对象 */ @Component //放入容器里不用设为静态方法 public class HostHolder { //key就是线程对象,值为线程的变量副本 private ThreadLocal<User> users = new ThreadLocal<>(); /** * 以线程为key存入User * @param user */ public void setUser(User user){ users.set(user); } /** * 从ThreadLocal线程中取出User * @return */ public User getUser(){ return users.get(); } /** * 释放线程 */ public void clear(){ users.remove(); } }
/**
* 通过凭证号找到凭证
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket){
return loginTicketMapper.selectOne(new LambdaQueryWrapper<LoginTicket>()
.eq(LoginTicket::getTicket, ticket));
}
package com.qiuyu.controller.interceptor; /** * 登录凭证拦截器,用于根据凭证号获取用户,并传给视图 */ @Component public class LoginTicketInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Autowired private HostHolder hostHolder; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从request中获取cookie 凭证 String ticket = CookieUtil.getValue(request, "ticket"); if (!StringUtils.isBlank(ticket)) { // 查询凭证 LoginTicket loginTicket = userService.findLoginTicket(ticket); // 检查凭证是否有效 if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { // 根据凭证查询用户 User user = userService.findUserById(String.valueOf(loginTicket.getUserId())); // 把用户存入ThreadLocal hostHolder.setUser(user); } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //在调用模板引擎之前,把user给model 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 { //最后把ThreadLocal中的当前user删除 hostHolder.clear(); } }
package com.qiuyu.config; /** * 拦截器配置类 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LoginTicketInterceptor loginTicketInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginTicketInterceptor) .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg"); } }
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
<a href="site/letter.html">消息<span >12</span></a>
</li>
th:if="${loginUser!=null}"
如果有loginUser传过来才显示th:if="${loginUser==null}"
如果有没有loginUser传过来才显示当前情况下,没登录也能够访问/user/setting,想要不让其访问,可以使用之前的那种拦截器,这里采用注解的方法
常用的元注解:
注意: 若有2个拦截器,拦截器执行顺序为注册在WebMvcConfig配置类中的顺序
package com.qiuyu.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
/** * 跳转设置页面 * @return */ @LoginRequired @GetMapping("/setting") public String getUserPage() { return "/site/setting"; } /** *上传头像 */ @LoginRequired @PostMapping("/upload") public String uploadHeader(MultipartFile headerImage, Model model) { }
拦截有注解,并且没登陆的那些请求
package com.qiuyu.controller.interceptor; /** * @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); //如果这个方法被@LoginRequired注解,并且未登录,跳转并拦截! if (loginRequired != null && hostHolder.getUser() == null) { response.sendRedirect(request.getContextPath()+"/login"); return false; } } return true; } }
package com.qiuyu.config; /** * 拦截器配置类 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LoginTicketInterceptor loginTicketInterceptor; @Autowired private LoginRequiredInterceptor loginRequiredInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginTicketInterceptor) .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); registry.addInterceptor(loginRequiredInterceptor) .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg"); } }
之前验证码使用kaptcha
生成后,就将字符存入了session中,等待验证
//生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//验证码存入session,用于验证用户输入是否正确
session.setAttribute("kaptcha",text);
如果使用分布式,分布式session会出现问题,这里使用redis存储
// 验证码
private static final String PREFIX_KAPTCHA = "kaptcha";
/**
* 登录验证码
* @param owner
* @return
*/
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
/** * 验证码生成 * @param response */ @GetMapping("/kaptcha") public void getKaptcha(HttpServletResponse response){ //生成验证码 String text = kaptchaProducer.createText(); BufferedImage image = kaptchaProducer.createImage(text); //优化前:将验证码存入session..... //session.setAttribute("kaptcha",text); //优化后:生成验证码的归属传给浏览器Cookie String kaptchaOwner = CommunityUtil.generateUUID(); Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner); cookie.setMaxAge(60); //s cookie.setPath(contextPath); response.addCookie(cookie); //优化后:将验证码存入Redis String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); redisTemplate.opsForValue().set(redisKey, text, 60 , TimeUnit.SECONDS); //将图片输出到浏览器 response.setContentType("image/png"); try { OutputStream os = response.getOutputStream(); ImageIO.write(image,"png",os); os.flush(); } catch (IOException e) { logger.error("响应验证码失败:"+e.getMessage()); } }
/** * 登录功能 * @param username * @param password * @param code 验证码 * @param rememberme 是否勾选记住我 * @param model * @param response 用于浏览器接受cookie * @return */ @PostMapping(path = "/login") public String login(String username, String password, String code, boolean rememberme, Model model, HttpServletResponse response, @CookieValue("kaptchaOwner") String kaptchaOwner){ //优化前:首先检验验证码(从session取验证码) //String kaptcha = (String) session.getAttribute("kaptcha"); String kaptcha = null; // 优化后:从redis中获取kaptcha的key if(!StringUtils.isBlank(kaptchaOwner)){ String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner); //获取redis中的验证码答案 kaptcha = (String) redisTemplate.opsForValue().get(redisKey); System.out.println(kaptcha); } if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){ //空值或者不相等 model.addAttribute("codeMsg","验证码不正确"); return "site/login"; } /* * 1.验证用户名和密码(重点) * 2.传入浏览器cookie=ticket */ int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS; Map<String, Object> map = loginService.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"; } }
之前在登录凭证拦截器中,每次用户访问都需要查询一次数据库,效率太低
//从request中获取cookie 凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (!StringUtils.isBlank(ticket)) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
}
直接使用redis,mysql中的凭证表无需使用
// 登录凭证
private static final String PREFIX_TICKET = "ticket";
/**
* 登录凭证
* @param ticket
* @return
*/
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
废弃LoginTicket数据库表,使用redis
//生成登录凭证(相当于记住我这个功能==session)
LoginTicket ticket = new LoginTicket();
ticket.setUserId(user.getId());
ticket.setTicket(CommunityUtil.generateUUID());
ticket.setStatus(0); //有效
//当前时间的毫秒数+过期时间毫秒数
ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
Date date = new Date();
// 优化前:loginTicketMapper.insertLoginTicket(ticket);
// 优化后:loginticket对象放入redis中
String redisKey = RedisKeyUtil.getTicketKey(ticket.getTicket());
// opsForValue将ticket对象序列化为json字符串
redisTemplate.opsForValue().set(redisKey, ticket);
/**
* 登出
* @param ticket 登录凭证
*/
public void logout(String ticket) {
//优化前:找到数据库中的ticket,把状态改为1
//优化后:loginticket对象从redis中取出后状态设为1后放回
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
//放回
redisTemplate.opsForValue().set(redisKey,loginTicket);
}
/**
* 通过凭证号找到凭证
*
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectOne(new LambdaQueryWrapper<LoginTicket>()
// .eq(LoginTicket::getTicket, ticket));
//redis优化后:从redis中取出
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
每次请求都需要根据凭证来获取用户信息,访问的频率非常高
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(String.valueOf(loginTicket.getUserId()));
// 把用户存入ThreadLocal
hostHolder.setUser(user);
}
优先从缓存中取值
取不到时,从数据库中取,初始化缓存数据(redis存值)
数据变更时清除缓存(也可更新缓存,但是多线程时有并发的问题)
private static final String PREFIX_USER = "user";
/**
* 用户缓存
* @param userId
* @return
*/
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
// 1.优先从缓存中取值 private User getCache(int userId) { String redisKey = RedisKeyUtil.getUserKey(userId); return (User) redisTemplate.opsForValue().get(redisKey); } // 2.取不到时初始化缓存数据(redis存值) private User initCache(int userId) { User user = userMapper.selectById(userId); String redisKey = RedisKeyUtil.getUserKey(userId); redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS); return user; } // 3.数据变更时清除缓存(删除redis的key) private void clearCache(int userId) { String redisKey = RedisKeyUtil.getUserKey(userId); redisTemplate.delete(redisKey); }
public User findUserById(String id) {
// return userMapper.selectById(Integer.parseInt(id));
//优先从缓存中取值
User user = getCache(Integer.parseInt(id));
if(user == null){
//取不到时,从数据库中取,然后初始化缓存数据(redis存值)
user = initCache(Integer.parseInt(id)); //乌鱼子,忘了写user=找bug找了一小时
}
return user;
}
/** * 激活账号 * * @param userId * @param activationCode * @return */ public int activate(int userId, String activationCode) { //根据userid获取用户信息 User user = userMapper.selectById(userId); if (user.getStatus() == 1) { //已经激活,则返回重复 return ACTIVATION_REPEAT; } else if (user.getActivationCode().equals(activationCode)) { //如果未激活,判断激活码是否相等 //激活账号 user.setStatus(1); // userMapper.updateById(user); //redis优化后 clearCache(userId); return ACTIVATION_SUCCESS; } else { //不相等 return ACTIVATION_FAILURE; } }
/**
* 更新用户头像路径
*
* @param userId
* @param headerUrl
* @return
*/
public int updateHeaderUrl(int userId, String headerUrl) {
User user = new User();
user.setId(userId);
user.setHeaderUrl(headerUrl);
int rows = userMapper.updateById(user);
clearCache(userId);
return rows;
}
注意:1. 必须是Post请求 2.表单:enctype=“multipart/form-data” 3.参数类型MultipartFile只能封装一个文件
上传路径可以是本地路径也可以是web路径
访问路径必须是符合HTTP协议的Web路径
/**
* 更新用户头像路径
* @param userId
* @param headerUrl
* @return
*/
public int updateHeaderUrl(int userId, String headerUrl) {
User user = new User();
user.setId(userId);
user.setHeaderUrl(headerUrl);
return userMapper.updateById(user);
}
MultipartFile
读取图片package com.qiuyu.controller; @Controller @RequestMapping("/user") public class UserController { public static final Logger logger = LoggerFactory.getLogger(UserController.class); @Value("${community.path.domain}") private String domain; @Value("${community.path.upload-path}") private String uploadPath; @Value("${server.servlet.context-path}") private String contextPath; @Autowired private UserService userService; @Autowired private HostHolder hostHolder; /** * 上传头像 * * @param headerImage * @param model * @return */ @PostMapping("/upload") public String uploadHeader(MultipartFile headerImage, Model model) { if (headerImage == null) { model.addAttribute("error", "您还没有选择图片!"); return "/site/setting"; } /* * 获得原始文件名字 * 目的是:生成随机不重复文件名,防止同名文件覆盖 * 方法:获取.后面的图片类型 加上 随机数 */ String filename = headerImage.getOriginalFilename(); int index = filename.lastIndexOf("."); String suffix = filename.substring( index+1); //任何文件都可以上传,根据业务在此加限制.这里为没有后缀不合法 if (StringUtils.isBlank(suffix) || index < 0) { model.addAttribute("error", "文件格式不正确!"); return "/site/setting"; } //生成随机文件名 filename = CommunityUtil.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.updateHeaderUrl(user.getId(), headerUrl); return "redirect:/index"; } /** * 得到服务器图片 * void:返回给浏览器的是特色的图片类型所以用void * * @param fileName * @param response */ @GetMapping("/header/{fileName}") 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()); } } }
/** * 更新密码 * @param userId * @param oldPassword * @param newPassword * @return map返回信息 */ public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) { Map<String, Object> map = new HashMap<>(); //空值判断 if(StringUtils.isBlank(oldPassword)){ map.put("oldPasswordMsg","原密码不能为空"); return map; } if(StringUtils.isBlank(newPassword)){ map.put("newPasswordMsg","新密码不能为空"); return map; } //根据userId获取对象 User user = userMapper.selectById(userId); //旧密码加盐,加密 oldPassword = CommunityUtil.md5(oldPassword+user.getSalt()); //判断密码是否相等 if(!user.getPassword().equals(oldPassword)){ //不相等,返回 map.put("oldPasswordMsg","原密码错误"); return map; } //新密码加盐,加密 newPassword = CommunityUtil.md5(newPassword+user.getSalt()); user.setPassword(newPassword); userMapper.updateById(user); //map为空表示修改成功 return map; }
从ThreadLocal中拿userid
/** * 更新密码 * @param oldPassword * @param newPassword * @param model * @return */ @LoginRequired @PostMapping("/updatePassword") public String updatePassword(String oldPassword, String newPassword,Model model){ User user = hostHolder.getUser(); Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword); if(map == null || map.isEmpty()){ //成功!使用重定向,不然报错 return "redirect:/index"; }else{ //失败 model.addAttribute("oldPasswordMsg",map.get("oldPasswordMsg")); model.addAttribute("newPasswordMsg",map.get("newPasswordMsg")); return "/site/setting"; } }
<input type="password" th:class="|form-control ${oldPasswordMsg!=null?'is-invalid':''}|"
th:value="${param.oldPassword!=null?param.oldPassword:''}"
id="old-password" name="oldPassword" placeholder="请输入原始密码!" required>
th:value="${param.oldPassword!=null?param.oldPassword:''}"
使用前缀树来存储敏感词 :
根节点不包含字符,除根节点以外的每个节点,只包含一个字符
从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应字符串
每个节点的所有子节点,包含的字符串不相同
核心 :
有一个指针1指向前缀树,用以遍历敏感词的每一个字符
有一个指针2指向被过滤字符串,用以标识敏感词的开头
有一个指针3指向被过滤字符串,用以标识敏感词的结尾
在resources创建sensitive-words.txt文敏感词文本
package com.qiuyu.utils; /** * 敏感词过滤器 */ @Component public class SensitiveFilter { private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class); // 替换符 private static final String REPLACEMENT = "***"; // 根节点 private TrieNode rootNode = new TrieNode(); // 构造器之后运行 @PostConstruct public void init() { try ( // 读取文件流 BufferedReader带缓冲区效率更高 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()); } } /** * 将一个敏感词添加到前缀树中 * 类似于空二叉树的插入 */ private void addKeyword(String keyword) { TrieNode tempNode = rootNode; for (int i = 0; i < keyword.length(); i++) { //将汉字转化为Char值 char c = keyword.charAt(i); //找下有没有这个子节点,没有的话加入 TrieNode subNode = tempNode.getSubNode(c); if (subNode == null) { // 初始化子节点并加入到前缀树中 subNode = new TrieNode(); tempNode.addSubNode(c, subNode); } // 指向子节点,进入下一轮循环 tempNode = subNode; // 设置结束标识 if (i == keyword.length() - 1) { tempNode.setKeywordEnd(true); } } } /** * 过滤敏感词 * @param text 待过滤的文本 * @return 过滤后的文本 */ public String filter(String text) { if (StringUtils.isBlank(text)) { return null; } // 指针1 TrieNode tempNode = rootNode; // 指针2 int begin = 0; // 指针3 int position = 0; // 结果(StringBuilder:可变长度的String类) StringBuilder sb = new StringBuilder(); //用position做结尾判断比begin指针少几次循环 while (position < text.length()) { char c = text.charAt(position); // 跳过符号,比如 ☆赌☆博☆ if (isSymbol(c)) { // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步(就是不理他) if (tempNode == rootNode) { sb.append(c); begin++; } // 无论符号在开头或中间,指针3都向下走一步 position++; continue; } // 检查下级节点 tempNode = tempNode.getSubNode(c); if (tempNode == null) { // 以begin开头的字符串不是敏感词,直接加入结果 sb.append(text.charAt(begin)); // 进入下一个位置 position = ++begin; // 重新指向根节点 tempNode = rootNode; } else if (tempNode.isKeywordEnd()) { // 发现敏感词,将begin~position字符串替换掉 sb.append(REPLACEMENT); // 进入下一个位置 begin = ++position; // 重新指向根节点 tempNode = rootNode; } else { // 检查下一个字符 position++; } } // 将最后一批字符计入结果 sb.append(text.substring(begin)); return sb.toString(); } // 判断是否为符号 private boolean isSymbol(Character c) { // isAsciiAlphanumeric判断是否为字母或数字 // 0x2E80~0x9FFF 是东亚文字范围 return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); } // 内部类构造前缀树数据结构 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); } } }
核心 :ajax异步:整个网页不刷新,访问服务器资源返回结果,实现局部的刷新。
实质:JavaScript和XML(但目前JSON的使用比XML更加普遍)
导入FastJson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
/** * 使用fastjson,将JSON对象转为JSON字符串(前提要引入Fastjson) * @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) { //从map里的key集合中取出每一个key 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); }
<input type="button" value="发送" onclick="send();"> //异步JS function send() { $.post( "/community/test/ajax", {"name":"张三","age":25}, //回调函数返回结果 function(data) { console.log(typeof (data)); console.log(data); //json字符串转js对象 data = $.parseJSON(data); console.log(typeof (data)); console.log(data.code); console.log(data.msg); } ) }
/**
* Ajax异步请求示例
*/
@RequestMapping(value = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(200,"操作成功!");
}
/** * 新增一条帖子 * @param post 帖子 * @return */ public int addDiscussPost(DiscussPost post){ if(post == null){ //不用map直接抛异常 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.insert(post); }
/** * 添加帖子 * @param title 标题 * @param content 内容 * @return */ @PostMapping("/add") @ResponseBody // @LoginRequired public String addDiscussPost(String title, String content){ //获取当前登录的用户 User user = hostHolder.getUser(); if (user == null){ //403权限不够 return CommunityUtil.getJSONString(403,"你还没有登录哦!"); } if(StringUtils.isBlank(title) || StringUtils.isBlank(content)){ return CommunityUtil.getJSONString(222,"贴子标题或内容不能为空!"); } DiscussPost post = new DiscussPost(); post.setUserId(user.getId().toString()); post.setTitle(title); post.setContent(content); post.setType(0); post.setStatus(0); post.setCreateTime(new Date()); //业务处理,将用户给的title,content进行处理并添加进数据库 discussPostService.addDiscussPost(post); //返回Json格式字符串给前端JS,报错的情况将来统一处理 return CommunityUtil.getJSONString(0,"发布成功!"); }
注意:$.parseJSON(data) →通过jQuery,将服务端返回的JSON格式的字符串转为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", //与Controller层两个属性要一致!!! {"title":title,"content":content}, function(data) { //把json字符串转化成Js对象,后面才可以调用data.msg data = $.parseJSON(data); // 在提示框中显示返回消息 $("#hintBody").text(data.msg); // 显示提示框 $("#hintModal").modal("show"); // 2秒后,自动隐藏提示框 setTimeout(function(){ $("#hintModal").modal("hide"); // 成功,刷新页面 if(data.code == 0) { window.location.reload(); } }, 2000); } ); }
/**
* 通过id查找帖子
* @param id
* @return
*/
public DiscussPost findDiscussPostById(int id){
return discussPostMapper.selectById(id);
}
/** * 查看帖子详细页 * @param discussPostId * @param model * @return */ @GetMapping( "/detail/{discussPostId}") public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){ //通过前端传来的Id查询帖子 DiscussPost post = discussPostService.findDiscussPostById(discussPostId); model.addAttribute("post",post); //用以显示发帖人的头像及用户名 User user = userService.findUserById(post.getUserId()); model.addAttribute("user",user); return "/site/discuss-detail"; }
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">标题</a>
@{ }
中想要常量和变量的拼接需要用两个| |
<b th:text="${#dates.format(post.createTime,'yyyy-MM-dd HH:mm:ss')}">时间</b>
#dates.format()
用于格式化时间第一类丢失更新
第二类丢失更新
脏读
不可重复读
幻读
事务1的回滚导致事务2更新的数据丢失了
方法:
**1.通过XML配置 **
2.通过注解@Transaction,如下:
/* REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务
* REQUIRED_NEW: 创建一个新事务,并且暂停当前事务(外部事务)
* NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会和REQUIRED一样
* 遇到错误,Sql回滚 (A->B)
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
propagation
用于配置事务传播机制,既两个带事务的方法AB,方法A调用方法B,事务以哪个为准
REQUIRED
外部事务为准,比如A调用B,以A为准,如果A有事务就按照A的事务来,如果A没有事务就创建一个新的事务
REQUIRED_NEW
创建一个新的事务,比如A调用B,直接无视(暂停)A的事务,B自己创建一个新的事务
NESTED
嵌套,如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则自己创建新事务
控制粒度更低,比如一个方法要访问10次数据库,只有5次需要保证事务,就可以用编程式来控制,声明式会10次全都放入事务中
方法: 通过TransactionTemplate组件执行SQL管理事务,如下:
public Object save2(){ transactionTemplate. setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); transactionTemplate. setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); //回调函数 return transactionTemplate.execute(new TransactionCallback<Object>() { @Override public Object doInTransaction(TransactionStatus status) { User user = new User(); user.setUsername("Marry"); user.setSalt(CommunityUtil.generateUUID().substring(0,5)); user.setPassword(CommunityUtil.md5("123123")+user.getSalt()); user.setType(0); user.setHeaderUrl("http://localhost:8080/2.png"); user.setCreateTime(new Date()); userMapper.insertUser(user); //设置error,验证事务回滚 Integer.valueOf("abc"); return "ok"; } }); }
package com.qiuyu.service; @Service public class CommentService { @Autowired private CommentMapper commentMapper; /** * 分页获得指定帖子的评论 * @param entityType * @param entityId * @param page * @return */ public IPage<Comment> findCommentsByEntity(int entityType, int entityId, IPage<Comment> page) { LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Comment::getEntityType, entityType).eq(Comment::getEntityId, entityId); commentMapper.selectPage(page,wrapper); return page; } /** * 获取某个帖子评论的数量 * @param entityType * @param entityId * @return */ public int findCommentCount(int entityType, int entityId){ Integer count = commentMapper.selectCount(new LambdaQueryWrapper<Comment>() .eq(Comment::getEntityType, entityType) .eq(Comment::getEntityId, entityId)); return count; } }
/** * 查看帖子详细页 * @param discussPostId * @param model * @return */ @GetMapping( "/detail/{discussPostId}") public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, MyPage<Comment> page){ //通过前端传来的Id查询帖子 DiscussPost post = discussPostService.findDiscussPostById(discussPostId); model.addAttribute("post",post); //用以显示发帖人的头像及用户名 User user = userService.findUserById(post.getUserId()); model.addAttribute("user",user); //得到帖子的评论 page.setSize(5); page.setPath("/discuss/detail/"+discussPostId); page = (MyPage<Comment>) commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page); //评论列表 List<Comment> commentList = page.getRecords(); // 评论: 给帖子的评论 // 回复: 给评论的评论 // 评论VO(viewObject)列表 (将comment,user信息封装到每一个Map,每一个Map再封装到一个List中) List<Map<String,Object>> commentVoList = new ArrayList<>(); if(commentList != null){ for (Comment comment : commentList) { //一条评论的VO Map<String, Object> commentVo = new HashMap<>(10); //评论 commentVo.put("comment",comment); //评论作者 commentVo.put("user",userService.findUserById(comment.getUserId().toString())); //回复 Page<Comment> replyPage = new Page<>(); replyPage.setCurrent(1); replyPage.setSize(Integer.MAX_VALUE); replyPage = (Page<Comment>) commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), replyPage); //回复列表 List<Comment> replyList = replyPage.getRecords(); //回复的VO列表 List<Map<String,Object>> replyVoList = new ArrayList<>(); if(replyList != null){ for (Comment reply : replyList) { //一条回复的VO Map<String, Object> replyVo = new HashMap<>(10); //回复 replyVo.put("reply",reply); //回复的作者 replyVo.put("user",userService.findUserById(reply.getUserId().toString())); //回复给谁 User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId().toString()); replyVo.put("target",target); replyVoList.add(replyVo); } } //回复列表放入评论 commentVo.put("reply",replyVoList); //评论的回复数量 int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId()); commentVo.put("replyCount",replyCount); commentVoList.add(commentVo); } } model.addAttribute("comments",commentVoList); model.addAttribute("page",page); return "/site/discuss-detail"; }
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="commentvo:${comments}">
th:each
循环<span th:text="${(page.current-1) * page.size + commentvoStat.count}">1</span>#
commentvoStat.count
循环中默认带一个循环属性名+Stat的对象,使用count可以得到目前循环到第几个<a th:href="|#huifu-${replyvoStat.count}|" data-toggle="collapse" >回复</a>
<div th:id="|huifu-${replyvoStat.count}|" class="mt-4 collapse"></div>
/**
* 根据帖子id修改帖子的评论数量
* @param id
* @param commentCount
* @return
*/
public int updateCommentCount(int id, int commentCount) {
DiscussPost discussPost = new DiscussPost();
discussPost.setId(id);
discussPost.setCommentCount(commentCount);
return discussPostMapper.updateById(discussPost);
}
/** * 添加评论(涉及事务) * 先添加评论,后修改discuss_post中的评论数(作为一个整体事务,出错需要整体回滚!) * @param comment * @return */ @Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED) public int addComment(Comment comment){ if(comment == null){ throw new IllegalArgumentException("参数不能为空!"); } /**添加评论**/ //过滤标签 comment.setContent(HtmlUtils.htmlEscape(comment.getContent())); //过滤敏感词 comment.setContent(sensitiveFilter.filter(comment.getContent())); //添加评论 int rows =commentMapper.insert(comment); /** * 更新帖子评论数量 * 如果是帖子类型才更改帖子评论数量,并且获取帖子评论的id */ if(comment.getEntityType() == ENTITY_TYPE_POST){ //评论数 int count = findCommentCount(comment.getEntityType(), comment.getEntityId()); //更新数量 discussPostService.updateCommentCount(comment.getEntityId(),count); } return rows; }
评论帖子
<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|}">
<textarea placeholder="在这里畅所欲言你的看法吧!" name="content"></textarea>
<input type="hidden" name="entityType" th:value="1">
<input type="hidden" name="entityId" th:value="${post.id}">
<input type="hidden" name="targetId" th:value="0">
</form>
package com.qiuyu.dao; @Mapper public interface MessageMapper extends BaseMapper<Message> { /** * 分页查询出当前用户的所有会话,以及会话中最新的一条消息 * @param userId * @param page * @return */ IPage<Message> selectConversations(@Param("userId") Integer userId, IPage<Message> page); /** * 查询当前用户的会话数量 * @param userId * @return */ int selectConversationCount(@Param("userId") int userId); /** * 查询某个会话所包含的私信列表 * @param conversationId * @param page * @return */ IPage<Message> selectLetters(@Param("conversationId") String conversationId, IPage<Message> page); /** * 查询某个会话所包含的私信数量 * @param conversationId * @return */ int selectLetterCount(@Param("conversationId") String conversationId); /** * 查询未读的数量 * 1.带参数conversationId :私信未读数量 * 2.不带参数conversationId :当前登录用户 所有会话未读数量 */ int selectLetterUnreadCount(@Param("userId")int userId,@Param("conversationId") String conversationId); }
<?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.qiuyu.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 community.message where id in ( select max(id) from community.message where status != 2 and from_id != 1 and (from_id = #{userId} or to_id = #{userId}) group by conversation_id ) order by id DESC </select> <select id="selectConversationCount" resultType="int"> select count(m.maxid) from (select max(id) as maxid from community.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 community.message where status != 2 and from_id != 1 and conversation_id = #{conversationId} order by id asc </select> <select id="selectLetterCount" resultType="int"> select count(id) from community.message where status != 2 and from_id != 1 and conversation_id = #{conversationId} </select> <select id="selectLetterUnreadCount" resultType="int"> select count(id) from community.message where status = 0 and from_id != 1 and to_id = #{userId} <if test="conversationId!=null"> and conversation_id = #{conversationId} </if> </select> </mapper>
package com.qiuyu.service; @Service public class MessageService { @Autowired private MessageMapper messageMapper; /** * 查询当前用户的会话列表,每个会话只返回一条最新消息 * @param userId * @param page * @return */ public IPage<Message> findConversations(int userId, IPage<Message> page) { return page = messageMapper.selectConversations(userId, page); } /** * 查询当前用户的会话数量 * @param userId * @return */ public int findConversationCount(int userId) { return messageMapper.selectConversationCount(userId); } /** * 查询某个会话中包含的所有消息 * @param conversationId * @param page * @return */ public IPage<Message> findLetters(String conversationId, IPage<Message> page) { return messageMapper.selectLetters(conversationId, page); } /** * 查询某个会话中包含的消息数量 * @param conversationId * @return */ public int findLetterCount(String conversationId) { return messageMapper.selectLetterCount(conversationId); } /** * 查询未读的私信的数量 * @param userId * @param conversationId * @return */ public int findLetterUnreadCount(int userId, String conversationId) { return messageMapper.selectLetterUnreadCount(userId, conversationId); } }
package com.qiuyu.controller; @Controller @RequestMapping("/letter") public class MessageController { @Autowired private MessageService messageService; @Autowired private HostHolder hostHolder; @Autowired private UserService userService; @LoginRequired @GetMapping("/list") public String getLetterList(Model model, MyPage<Message> page){ User user = hostHolder.getUser(); //分页信息 page.setSize(5); page.setPath("/letter/list"); //会话列表 page = (MyPage<Message>) messageService.findConversations(user.getId(),page); List<Message> conversationList = page.getRecords(); //VO List<Map<String,Object>> conversationVo = new ArrayList<>(); if(conversationList != null){ for (Message message : conversationList) { Map<String, Object> map = new HashMap<>(); map.put("conversation",message); //会话中的消息数 map.put("letterCount", messageService.findLetterCount(message.getConversationId())); //未读消息数 map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId())); //显示的目标用户 Integer targetId = user.getId().equals(message.getFromId()) ? message.getToId() : message.getFromId(); map.put("target",userService.findUserById(targetId.toString())); conversationVo.add(map); } } model.addAttribute("conversations", conversationVo); // 当前登录用户总未读条数 int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null); model.addAttribute("letterUnreadCount", letterUnreadCount); model.addAttribute("page",page); return "/site/letter"; } }
@LoginRequired @GetMapping("/detail/{conversationId}") public String getLetterDetail(@PathVariable("conversationId") String conversationId, Model model, MyPage<Message> page) { //分页信息 page.setSize(5); page.setPath("/letter/detail/" + conversationId); //获取私信信息 page = (MyPage<Message>) messageService.findLetters(conversationId, page); List<Message> letterList = page.getRecords(); //VO List<Map<String, Object>> letterVo = 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().toString())); letterVo.add(map); } } model.addAttribute("letters", letterVo); //获取私信目标 model.addAttribute("target", getLetterTarget(conversationId)); model.addAttribute("page", page); return "/site/letter-detail"; } /** * 封装获取目标会话用户(将如:101_107拆开) * * @param conversationId * @return */ private User getLetterTarget(String conversationId) { String[] s = conversationId.split("_"); Integer id0 = Integer.parseInt(s[0]); Integer id1 = Integer.parseInt(s[1]); //不是会话中的用户 int userId = hostHolder.getUser().getId(); if(userId != id0 && userId != id1){ throw new IllegalArgumentException("无权限查看"); } //当前用户是哪个就选另一个 Integer target = hostHolder.getUser().getId().equals(id0) ? id1 : id0; return userService.findUserById(target.toString()); }
<button type="button" onclick="back();">返回</button>
<script>
function back(){
location.href = CONTEXT_PATH + "/letter/list"
}
</script>
/**
* 插入会话
* @param message
* @return
*/
int insertMessage(Message message);
/**
* 批量更改每个会话的所有未读消息为已读
* @param ids
* @param statuss
* @return
*/
int updateStatus(@Param("ids") List<Integer> ids,@Param("status") int status);
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into community.message(<include refid="insertFields"></include>)
values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
<update id="updateStatus">
update community.message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
/** * 添加消息 * @param message * @return */ public int addMessage(Message message){ //转义标签 message.setContent(HtmlUtils.htmlEscape(message.getContent())); //过滤敏感词 message.setContent(sensitiveFilter.filter(message.getContent())); return messageMapper.insertMessage(message); } /** * 把多个消息都设为已读 * @param ids * @return */ public int readMessage(List<Integer> ids){ return messageMapper.updateStatus(ids, 1); }
@LoginRequired @GetMapping("/detail/{conversationId}") public String getLetterDetail(@PathVariable("conversationId") String conversationId, Model model, MyPage<Message> page) { /** * 以上省略。。。。。。 */ //消息设置已读(当打开这个页面是就更改status =1) List<Integer> ids = getLetterIds(letterList); if (!ids.isEmpty()) { messageService.readMessage(ids); } } /** * 获得批量私信的未读数id * @param letterList * @return */ private List<Integer> getLetterIds(List<Message> letterList){ List<Integer> ids = new ArrayList<>(); if (letterList != null) { for (Message message : letterList) { //只有当前登录用户与message列表中目标用户一致并且staus = 0 时才是未读数,加入未读私信集合 if (hostHolder.getUser().getId().equals(message.getToId()) && message.getStatus() == 0) { ids.add(message.getId()); } } } return ids; }
/** * 发送消息 * @param toName * @param content * @return */ @PostMapping("/send") @ResponseBody public String sendLetter(String toName, String content){ //获得发送目标 User target = userService.findUserByName(toName); if (target == null){ return CommunityUtil.getJSONString(1,"目标用户不存在!"); } //设置message属性 Message message = new Message(); message.setFromId(hostHolder.getUser().getId()); message.setToId(target.getId()); message.setContent(content); message.setCreateTime(new Date()); // conversationId (如101_102: 小_大) if (message.getFromId() < message.getToId()) { message.setConversationId(message.getFromId() + "_" +message.getToId()); }else{ message.setConversationId(message.getToId() + "_" +message.getFromId()); } messageService.addMessage(message); return CommunityUtil.getJSONString(0,"发送消息成功!"); }
异常都会扔到表现层中,所以只要处理Controller层就行了
注意:springboot默认在templates资源路径下面新建error目录,添加404.html和500.html页面就会自动配置上错误页面自动跳转
//annotations只扫描带Controller注解的Bean @ControllerAdvice(annotations = Controller.class) public class ExceptionAdvice { public static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class); //发生异常时会被调用 @ExceptionHandler 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 xRequestedWith = request.getHeader("x-requested-with"); // XMLHttpRequest: Json格式字符串 if ("XMLHttpRequest".equals(xRequestedWith)) { // 要求以JSON格式返回 response.setContentType("application/plain;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(CommunityUtil.getJSONString(1, "服务器异常!")); } else { //普通请求直接重定向到错误页面 response.sendRedirect(request.getContextPath() + "/error"); } } }
@ExceptionHandler
异常处理来做日志
编译,类装载,运行时,都能进行织入
我们想要插入的代码放在**切面(Aspect)**中
切面中的代码放入目标对象的过程称为织入(Weaving)
切面中的代码织入目标对象的位置称为连接点(Joinpoint)
Pointcut用来指明切面中的代码要放到目标对象的哪些地方(连接点)
**通知(Advice)**指明织入到目标对象时的逻辑(在连接点的前后左右这些)
常见的使用场景有:权限检查、记录日志、事务管理
先导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Component @Aspect public class DemoAspect { // 返回值 包名.类名.方法名(方法参数) *表示所有 ..表示全部参数 @Pointcut("execution(* com.qiuyu.demonowcoder.service.*.*(..))") public void pointcut(){} //切点方法之前执行(常用) @Before("pointcut()") public void before(){ System.out.println("before"); } @After("pointcut()") public void after(){ System.out.println("after"); } /**返回值以后执行**/ @AfterReturning("pointcut()") public void afterRetuning() { System.out.println("afterRetuning"); } /**抛出异常以后执行**/ @AfterThrowing("pointcut()") public void afterThrowing() { System.out.println("afterThrowing"); } /**切点的前和后都可以执行**/ @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ System.out.println("around before"); Object obj = joinPoint.proceed(); System.out.println("around after"); return obj; } }
实现需求 :用户ip[1.2.3.4],在[时间],访问了[com.qiuyu.service.xxx()].
package com.qiuyu.aspect; @Component @Aspect public class ServiceLogAspect { public static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class); @Pointcut("execution(* com.qiuyu.service.*.*(..))") public void pointCut(){} @Before("pointCut()") public void before(JoinPoint joinPoint){ // 用户ip[1.2.3.4],在[时间],访问了[com.qiuyu.service.xxx()]. // 通过RequestContextHolder工具类获取request ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 通过request.getRemoteHost获取当前用户ip String ip = request.getRemoteHost(); String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); /** * joinPoint.getSignature().getDeclaringTypeName()-->得到类名com.qiuyu.service.* * joinPoint.getSignature().getName() -->方法名 */ String target = joinPoint.getSignature().getDeclaringTypeName() + "." +joinPoint.getSignature().getName(); // String.format()加工字符串 logger.info(String.format("用户[%s],在[%s],访问了[%s]业务.", ip, time, target)); } }
redis官方只提供linux版本
下载微软提供的redis
https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100
redis-cli
连接redis
select [0-11]
选择使用的库,redis一共12个库
flushdb
把数据刷新(删除)
keys *
查看所有的key
keys test*
查看test开头的key
type test:teachers
查看某个key的类型
exists test:teachers
查看某个key是否存在
del test:teachers
删除某个key
expire test:ids 5
五秒后key过期,删除
redis中两个单词之间的分割不是驼峰也不是下划线,建议使用冒号:
set test:count 1 #添加数据,设置test:count的值为1,1的类型为字符串
get test:count #获取数据,得到结果为"1"
incr test:count #指定数据加一,结果为(integer) 2
decr test:count #指定数据减一,结果为(integer) 1
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
127.0.0.1:6379> hset test:user id 1
(integer) 1
127.0.0.1:6379> hset test:user username zhangsan
(integer) 1
127.0.0.1:6379> hget test:user id
"1"
127.0.0.1:6379> hget test:user username
"zhangsan"
redis中的list为双端队列,左右都可存取
127.0.0.1:6379> lpush test:ids 101 102 103 #左侧依次放入 (integer) 3 127.0.0.1:6379> llen test:ids #列表长度 (integer) 3 127.0.0.1:6379> lindex test:ids 0 #根据索引查找 "103" 127.0.0.1:6379> lrange test:ids 0 2 #查看索引范围内的元素 1) "103" 2) "102" 3) "101" 127.0.0.1:6379> rpush test:ids 100 #右端插入 (integer) 4 127.0.0.1:6379> lpop test:ids #左侧弹出一个元素 "103" 127.0.0.1:6379> rpop test:ids #右侧弹出一个元素 "100"
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
127.0.0.1:6379> sadd test:teachers aaa bbb ccc #放入集合
3
127.0.0.1:6379> scard test:teachers #查看个数
3
127.0.0.1:6379> spop test:teachers #随机弹出一个
ccc
127.0.0.1:6379> smembers test:teachers #查看所有元素
bbb
aaa
Redis 有序集合和集合(set)一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
127.0.0.1:6379> zadd test:students 10 aaa 20 bbb 30 ccc 40 ddd 50 eee # 插入需要写分数
5
127.0.0.1:6379> zcard test:students #查看个数
5
127.0.0.1:6379> zscore test:students bbb #查看指定的元素的分数
20
127.0.0.1:6379> zrank test:students bbb #查看指定元素的排名(从0开始)
1
127.0.0.1:6379> zrange test:students 0 2 #按照分数,由小到大排序,第0-2个
aaa
bbb
ccc
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency
spring:
#redis
redis:
database: 11 #16个库用哪个
host: localhost
port: 6379
自带的RedisTemplate为Objtct,Object类型 我们这里使用String,Object就行
package com.qiuyu.config; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String,Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //设置key的序列化方法 template.setKeySerializer(RedisSerializer.string()); //设置value的序列化方法 template.setKeySerializer(RedisSerializer.json()); //设置hash的key序列化方式 template.setKeySerializer(RedisSerializer.string()); //设置hash的value序列化方式 template.setHashValueSerializer(RedisSerializer.json()); //让配置生效 template.afterPropertiesSet(); return template; } }
建议用下面这个
package com.qiuyu.config; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String,Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); //让配置生效 template.afterPropertiesSet(); return template; } }
@SpringBootTest @RunWith(SpringRunner.class) public class RedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedis(){ String redisKey = "test:redis"; redisTemplate.opsForValue().set(redisKey,1); System.out.println(redisTemplate.opsForValue().get(redisKey)); } //Hash @Test public void testHash(){ String redisKey = "test:redis2"; redisTemplate.opsForHash().put(redisKey,"id",6); redisTemplate.opsForHash().put(redisKey,"username","qiuyu"); Object id = redisTemplate.opsForHash().get(redisKey, "id"); Object username = redisTemplate.opsForHash().get(redisKey, "username"); System.out.println(id); System.out.println(username); } //List @Test public void testList(){ String redisKey = "test:redis3"; 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().rightPop(redisKey)); System.out.println(redisTemplate.opsForList().rightPop(redisKey)); /* 3 103 [103, 102, 101] 101 102 */ } //Set @Test public void testSet(){ String redisKey = "test:redis4"; redisTemplate.opsForSet().add(redisKey,"bbb","ccc","aaa"); System.out.println(redisTemplate.opsForSet().size(redisKey)); System.out.println(redisTemplate.opsForSet().pop(redisKey)); System.out.println(redisTemplate.opsForSet().members(redisKey)); /* 3 bbb [aaa, ccc] */ } //Zset @Test public void testZSet(){ String redisKey = "test:redis5"; redisTemplate.opsForZSet().add(redisKey,"aaa",80); redisTemplate.opsForZSet().add(redisKey,"bbb",90); redisTemplate.opsForZSet().add(redisKey,"ccc",60); redisTemplate.opsForZSet().add(redisKey,"ddd",100); redisTemplate.opsForZSet().add(redisKey,"eee",50); System.out.println(redisTemplate.opsForZSet().size(redisKey)); System.out.println(redisTemplate.opsForZSet().score(redisKey,"bbb")); System.out.println(redisTemplate.opsForZSet().rank(redisKey,"bbb")); System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey,"bbb")); System.out.println(redisTemplate.opsForZSet().range(redisKey,0,2)); System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey,0,2)); /* 5 90.0 3 1 [eee, ccc, aaa] [ddd, bbb, aaa] */ } //Keys操作 @Test public void testKeys(){ redisTemplate.delete("aaa"); System.out.println(redisTemplate.hasKey("aaa")); redisTemplate.expire("test:redis",10, TimeUnit.SECONDS); } //多次复用Key @Test public void testBoundOperations(){ String redisKey = "test:count3"; BoundValueOperations operations = redisTemplate.boundValueOps(redisKey); operations.set(1); //报错 // operations.increment(); // operations.increment(); // operations.increment(); System.out.println(operations.get()); } //编程式事务 @Test public void testTransaction(){ Object obj = redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String redisKey = "test:tx"; //启用事务 operations.multi(); operations.opsForSet().add(redisKey,"zhangsan"); operations.opsForSet().add(redisKey,"lisi"); operations.opsForSet().add(redisKey,"wangwu"); //redis会把这些操作放在队列中.提交事务时才执行,所以此时还没有数据 System.out.println(operations.opsForSet().members(redisKey)); //提交事务 return operations.exec(); } }); System.out.println(obj); //[] //[1, 1, 1, [lisi, zhangsan, wangwu]] } }
用于获取统一格式化的Key
package com.qiuyu.utils;
public class RedisKeyUtil {
private static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
/**
* 某个实体的赞
* key= like:entity:entityType:entityId -> set(userId)
*/
public static String getEntityLikeKey(int entityType, int entityId){
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
}
package com.qiuyu.service; @Service public class LikeService { @Autowired private RedisTemplate redisTemplate; // 点赞 (记录谁点了哪个类型哪个留言/帖子id) public void like(int userId, int entityType, int entityId){ String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); //判断like:entity:entityType:entityId 是否有对应的 userId Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId); // 第一次点赞,第二次取消点赞 if (isMember){ // 若已被点赞(即entityLikeKey里面有userId)则取消点赞->将userId从中移除 redisTemplate.opsForSet().remove(entityLikeKey, userId); }else { redisTemplate.opsForSet().add(entityLikeKey, userId); } } // 查询某实体(帖子、留言)点赞的数量 --> scard like:entity:1:110 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); // 1:已点赞 , 0:赞 return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0; } }
package com.qiuyu.controller; @Controller public class LikeController { @Autowired private LikeService likeService; @Autowired private HostHolder hostHolder; @GetMapping("/like") @ResponseBody public String like(int entityType, int entityId){ User user = hostHolder.getUser(); // 点赞 likeService.like(user.getId(), entityType,entityId); // 获取对应帖子、留言的点赞数量 long entityLikeCount = likeService.findEntityLikeCount(entityType, entityId); // 获取当前登录用户点赞状态(1:已点赞 0:赞) int entityLikeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId); Map<String,Object> map = new HashMap<>(); map.put("likeCount",entityLikeCount); map.put("likeStatus",entityLikeStatus); return CommunityUtil.getJSONString(0,null,map); } }
弃用href,使用href="javascript:;"
写法,如果直接删掉,鼠标放上去不会变为手形
使用onclick()
<a href="javascript:;" th:οnclick="|like(this,1,${post.id})|" class="text-primary" >
<b>赞</b> <span>11</span>
</a>
function like(btn, entityType, entityId) { $.post( CONTEXT_PATH + "/like", {"entityType":entityType,"entityId":entityId}, function(data) { data = $.parseJSON(data); if(data.code == 0){ //点赞成功,通过子节点得到span $(btn).children("span").text(data.likeCount); $(btn).children("b").text(data.likeStatus==1?"已赞":"点赞"); }else{ alert(data.msg); } } ); }
还需要把首页和帖子页面进行修改,在进入页面时候读取点赞数和状态
如果要查询某个人的被点赞数量,需要查到这个人的所有帖子,然后把每个帖子点赞数加起来,有点麻烦
我们可以添加一个维度,点赞的时候在redis中记录被点赞用户的被点赞个数
获取统一格式的key
k:v = like:user:userId -> set(userId)
public class RedisKeyUtil {
...
private static final String PREFIX_USER_LIKE = "like:user";
...
/**
* 某个用户收到的赞
* @param userId
* @return
*/
public static String getUserLikeKey(int userId){
return PREFIX_USER_LIKE + SPLIT + userId;
}
}
一是更新帖子/评论点赞数而是更新用户的被点赞数
使用事务进行控制
/** * 点赞 (记录谁点了哪个类型哪个留言/帖子id) * 同时给用户的点赞数加一 * 因为要进行多个操作,采用事务 * @param userId * @param entityType * @param entityId * @param entityUserId 实体的作者的Id,这里在页面直接传进来,不然使用数据库查太慢了 */ public void like(int userId, int entityType, int entityId, int entityUserId){ redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId); String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId); //查询需要在事务之外 Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId); //开启事务 operations.multi(); // 第一次点赞,第二次取消点赞 if (isMember){ // 若已被点赞,实体类移除点赞者,实体作者点赞数-1 redisTemplate.opsForSet().remove(entityLikeKey, userId); redisTemplate.opsForValue().decrement(userLikeKey); }else { redisTemplate.opsForSet().add(entityLikeKey, userId); redisTemplate.opsForValue().increment(userLikeKey); } //提交事务 return operations.exec(); } }); } //查询用户的点赞数 public long findUserLikeCount(int userId){ String userLikeKey = RedisKeyUtil.getUserLikeKey(userId); Long count = (Long) redisTemplate.opsForValue().get(userLikeKey); return count == null ? 0 : count; }
修改LikeController,加入entityUserId参数
@PostMapping("/like") @ResponseBody public String like(int entityType, int entityId,int entityUserId){ User user = hostHolder.getUser(); // 点赞 likeService.like(user.getId(), entityType,entityId,entityUserId); // 获取对应帖子、留言的点赞数量 long entityLikeCount = likeService.findEntityLikeCount(entityType, entityId); // 获取当前登录用户点赞状态(1:已点赞 0:赞) int entityLikeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId); Map<String,Object> map = new HashMap<>(); map.put("likeCount",entityLikeCount); map.put("likeStatus",entityLikeStatus); return CommunityUtil.getJSONString(0,null,map); }
th:onclike传入两个参数会报错
th:data-*
<a href="javascript:;" th:data-id="${post.id}" th:data-userId="${post.userId}"
th:onclick="|like(this,1,this.getAttribute('data-id'),this.getAttribute('data-userId'))|">
||
去掉,用[[ ]]
将参数包围<a href="javascript:;" th:onclick="like(this,1,[[${post.id}]],[[${post.userId}]])">
function like(btn, entityType, entityId, entityUserId) { $.post( CONTEXT_PATH + "/like", {"entityType": entityType, "entityId": entityId, "entityUserId": entityUserId}, function (data) { data = $.parseJSON(data); if (data.code == 0) { //点赞成功,通过子节点得到span $(btn).children("span").text(data.likeCount); $(btn).children("b").text(data.likeStatus == 1 ? "已赞" : "赞"); } else { alert(data.msg); } } ); }
@GetMapping("/profile/{userId}")
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(String.valueOf(userId));
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
// 进入某用户主页获取他(我)的点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);
return "/site/profile";
}
package com.qiuyu.utils; public class RedisKeyUtil { // 关注 private static final String PREFIX_FOLLOWEE = "followee"; // 粉丝 private static final String PREFIX_FOLLOWER = "follower"; /** * 某个用户关注的实体(用户,帖子) * followee:userId:entityType --> zset(entityId, date) */ public static String getFolloweeKey(int userId, int entityType) { return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType; } /** * 某个实体拥有的用户粉丝 * follower:entityType:entityId -->zset(userId, date) */ public static String getFollowerKey(int entityType, int entityId) { return PREFIX_FOLLOWER + SPLIT +entityType + SPLIT +entityId; } }
package com.qiuyu.service; @Service public class FollowService { @Autowired private RedisTemplate redisTemplate; /** * 关注某个实体 * @param userId * @param entityType * @param entityId */ public void follow(int userId, int entityType, int entityId) { redisTemplate.execute(new SessionCallback(){ @Override public Object execute(RedisOperations operations) throws DataAccessException { String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); operations.multi(); /** * System.currentTimeMillis()->用于获取当前系统时间,以毫秒为单位 * 关注时,首先将实体(用户或帖子)id添加用户关注的集合中,再将用户id添加进实体粉丝的集合中 */ redisTemplate.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis()); redisTemplate.opsForZSet().add(followerKey,userId,System.currentTimeMillis()); return operations.exec(); } }); } /** * 取消关注 * @param userId * @param entityType * @param entityId */ public void unfollow(int userId, int entityType, int entityId) { redisTemplate.execute(new SessionCallback(){ @Override public Object execute(RedisOperations operations) throws DataAccessException { String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); operations.multi(); //关注时,首先将实体(用户或帖子)id移除用户关注的集合中,再将用户id移除进实体粉丝的集合中 redisTemplate.opsForZSet().remove(followeeKey,entityId); redisTemplate.opsForZSet().remove(followerKey,userId); return operations.exec(); } }); } /** * 某个用户的关注的实体数量 * @param userId * @param entityType * @return */ public long findFolloweeCount(int userId, int entityType) { String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType); return redisTemplate.opsForZSet().zCard(followeeKey); } /** * 查询某个实体的粉丝数 * @param entityType * @param entityId * @return */ public long findFollowerCount(int entityType, int entityId) { String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId); return redisTemplate.opsForZSet().zCard(followerKey); } /** * 当前用户是否关注了该实体 * userId->当前登录用户 entityType->用户类型 entityId->关注的用户id * @param userId * @param entityType * @param entityId * @return */ public boolean hasFollowed(int userId, int entityType, int entityId) { String followeeKey =RedisKeyUtil.getFolloweeKey(userId, entityType); //查下score是否为空 return redisTemplate.opsForZSet().score(followeeKey, entityId) != null; } }
package com.qiuyu.controller; @Controller public class FollowController { @Autowired private FollowService followService; @Autowired private HostHolder hostHolder; /** * 关注 * @param entityType * @param entityId * @return */ @PostMapping("/follow") @ResponseBody public String follow(int entityType, int entityId) { followService.follow(hostHolder.getUser().getId(), entityType, entityId); return CommunityUtil.getJSONString(0,"已关注"); } /** * 取消关注 * @param entityType * @param entityId * @return */ @PostMapping("/unfollow") @ResponseBody public String unfollow(int entityType, int entityId) { followService.unfollow(hostHolder.getUser().getId(), entityType, entityId); return CommunityUtil.getJSONString(0,"已取消关注"); } }
// 关注数量(这里只考虑关注用户类型的情况)
long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount", followeeCount);
// 粉丝数量
long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
model.addAttribute("followerCount", followerCount);
// 是否已关注 (必须是用户登录的情况)
boolean hasFollowed = false;
if (hostHolder.getUser() != null) {
hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);
<input type="hidden" id="entityId" th:value="${user.id}">
<button type="button"
th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
th:text="${hasFollowed?'已关注':'关注他'}"
th:if="${loginUser!=null && loginUser.id!=user.id}">
关注TA</button>
使用样式btn-info来判断关注还是取关
$(function () { $(".follow-btn").click(follow); }); function follow() { var btn = this; if ($(btn).hasClass("btn-info")) { // 关注TA $.post( CONTEXT_PATH + "/follow", // "entityId":$(btn).prev().val() 获取btn按钮上一个的值 {"entityType": 3, "entityId": $(btn).prev().val()}, function (data) { data = $.parseJSON(data); if (data.code == 0) { window.location.reload(); } else { alert(data.msg); } } ); } else { console.log(123); // 取消关注 $.post( CONTEXT_PATH + "/unfollow", {"entityType": 3, "entityId": $(btn).prev().val()}, function (data) { data = $.parseJSON(data); if (data.code == 0) { window.location.reload(); } else { alert(data.msg); } } ); } }
/** * 查询某用户关注的人 * @param userId * @return */ public List<Map<String, Object>> findFollowees(int userId, int offset, int limit){ String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER); // 按最新时间倒序查询目标用户id封装在set<Integet>中 Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1); if (targetIds == null) { return null; } // 将user信息Map和redis用户关注时间Map一起封装到list ArrayList<Map<String, Object>> list = new ArrayList<>(); for (Integer targetId: targetIds) { HashMap<String, Object> map = new HashMap<>(); // 用户信息map User user = userService.findUserById(String.valueOf(targetId)); map.put("user", user); // 目标用户关注时间map(将long型拆箱成基本数据类型) Double score = redisTemplate.opsForZSet().score(followeeKey, targetId); map.put("followTime", new Date(score.longValue())); list.add(map); } return list; } /** * 查询某用户粉丝列表 * @param userId * @param offset * @param limit * @return */ public List<Map<String, Object>> findFollowers(int userId, int offset, int limit){ String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId); // 按最新时间倒序查询目标用户id封装在set<Integet>中 Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1); if (targetIds == null) { return null; } // 将user信息Map和redis用户关注时间Map一起封装到list ArrayList<Map<String, Object>> list = new ArrayList<>(); for (Integer targetId: targetIds) { HashMap<String, Object> map = new HashMap<>(); // 用户信息map User user = userService.findUserById(targetId.toString()); map.put("user", user); // 目标用户关注时间map(将long型拆箱成基本数据类型) Double score = redisTemplate.opsForZSet().score(followerKey, targetId); map.put("followTime", new Date(score.longValue())); list.add(map); } return list; }
//查询某用户关注列表 @GetMapping("/followees/{userId}") public String getFollowees(@PathVariable("userId")int userId, Page page, Model model) { // 当前访问的用户信息 User user = userService.findUserById(String.valueOf(userId)); // Controller层统一处理异常 if (user == null) { throw new RuntimeException("该用户不存在!"); } model.addAttribute("user", user); // 设置分页信息 page.setLimit(3); 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"; } /** * 判端当前登录用户与关注、粉丝列表的关注关系 * @param userId * @return */ private Boolean hasFollowed(int userId) { if (hostHolder.getUser() == null) { return false; } // 调用当前用户是否已关注user实体Service return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId); } //查询某用户粉丝列表 @GetMapping("/followers/{userId}") public String getFollowers(@PathVariable("userId")int userId, Page page, Model model) { // 当前访问的用户信息 User user = userService.findUserById(String.valueOf(userId)); // Controller层统一处理异常 if (user == null) { throw new RuntimeException("该用户不存在!"); } model.addAttribute("user", user); // 设置分页信息 page.setLimit(3); 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"; } }
<span>关注了 <a th:href="@{|/followees/${user.id}|}" th:text="${followeeCount}">5</a> 人</span>
<span>关注者 <a th:href="@{|/followers/${user.id}|}" th:text="${followerCount}">123</a> 人</span>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。