An open API service indexing awesome lists of open source software.

https://github.com/xumingyu2018/demonowcoder

仿牛客网项目
https://github.com/xumingyu2018/demonowcoder

java spring-boot

Last synced: about 1 year ago
JSON representation

仿牛客网项目

Awesome Lists containing this project

README

          

# 仿牛客网项目学习

# 主页讨论区分页查询功能!

## 1.首先设计Dao层接口(实体类略)

**以下是查询功能不包括分页** **(其中userId在DiscussPost类中作为外键)**

```java
//查询
//userId=0为所有帖子,1为我的帖子
//每个参数必须加@Param("")
List selectDiscussPosts(@Param("userId") int userId,@Param("offset")int offset,@Param("limit")int limit);

//为分页查询服务的查询总条数
//给参数起别名,如果只有一个参数并且要在里使用,则必须加别名
int selectDiscussRows(@Param("userId")int userId);
```

```sql


id,user_id,title,content,type,status,create_time,comment_count,score


user_id,title,content,type,status,create_time,comment_count,score



select
from discuss_post
where status!=2

and user_id=#{userId}


order by type desc,create_time desc


order by type desc,score desc,create_time desc

limit #{offset},#{limit}



select count(id)
from discuss_post
where status!=2

and user_id=#{userId}

```

## 2.然后设计Service层调用Dao层接口

```java
@Autowired
private DiscussPostMapper discussPostMapper;

public List findDiscussPosts(int userId,int offset,int limit){
return discussPostMapper.selectDiscussPosts(userId,offset,limit);
}

public int findDiscussPostRows(int userId){
return discussPostMapper.selectDiscussRows(userId);
}
```

## 3.其次封装分页功能

**封装分页功能相关信息在Page类!!**

```java
public class Page {

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

public int getCurrent() {
return current;
}

public void setCurrent(int current) {
//要作输入判断
if (current>=1){
this.current = current;
}
}

public int getLimit() {
return limit;
}

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

public int getRows() {
return rows;
}

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

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

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

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

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

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

## 4.最后设计Controller层

```java
@Autowired
private DiscussPostService discussPostService;

@Autowired
private UserService userService;

@RequestMapping(value = "/index",method = RequestMethod.GET)
public String getIndexPage(Model model, Page page){//传入model参数是因为要返回值给View
/*方法调用前,springMVC自动实例化Model和Page,并将Page注入Model
在thymeleaf中可以直接访问Page对象中的数据 */

//分页
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/community/index");

//查询所有,起始为page.getOffset(),终止为page.getLimit()个帖子,
List list=discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());

/*将查询的post帖子和user用户名拼接后放入map中,最后把全部map放入新的List中,
因为UserId是外键,需要显示的是对应的名字即可 */
List> discussPost =new ArrayList<>();

if (list!=null){
for(DiscussPost post:list){
HashMap map = new HashMap<>();
// 将查询到的帖子放入map
map.put("post",post);
// 将发布帖子对应的用户id作为参数
User user = userService.findUser(post.getUserId());
// 将发帖子的所有用户放入map
map.put("user",user);
// 显示帖子点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);

//将组合的map放入List<>
discussPost.add(map);
}
}
model.addAttribute("discussPosts",discussPost);
return "/index";
}
```

## 5.前端页面设计(Thymeleaf)

### 5.1查询页面

```html


```

### 5.2分页功能页面

```html




```

# 注册登录功能

## 发送邮件

### 1.邮箱设置:启用SMTP服务

### 2.SpringEmail

#### 2.1配置xml文件

```xml

org.springframework.boot
spring-boot-starter-mail
2.6.6

```

#### 2.2在application.yml配置邮箱参数

```yaml
# 配置邮箱
spring:
mail:
host: smtp.qq.com
port: 465
username: xxx@qq.com //本网站的发送方
password: xxx //密码为生成授权码后给的密码
protocol: smtps
```

#### 2.3创建MailClient邮箱工具类

```java
@Component
public class MailClient {

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

@Autowired
private JavaMailSender javaMailSender;

@Value("${spring.mail.username}")//将yml的属性注入到from
private String from;

public void sendMail(String to,String subject,String content){
try {
//MimeMessage用于封装邮件相关信息
MimeMessage message = javaMailSender.createMimeMessage();
//需要一个邮件帮助器,负责构建MimeMessage对象
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
//支持HTML文本
helper.setText(content,true);
//发送邮件都有JavaMailSender来做
javaMailSender.send(helper.getMimeMessage());
}catch (MessagingException e){
logger.error("发送邮件失败:"+e.getMessage());
}
}
}
```

#### 2.4测试类

````java
@Autowired
private MailClient mailClient;

@Autowired
private TemplateEngine templateEngine;//注入HTML模板引擎类,模板格式化

@Test
public void testTextMail(){//发送文本类型邮件
mailClient.sendMail("xmy981022@163.com","Test","Welcome");
}

@Test
public void testHTMLMail(){//发送thymeleaf html类型文件
Context context = new Context();
context.setVariable("username","Nevermore");
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail("xmy981022@163.com","HTML",content);
}

注意:JavaMailSender和TemplateEngine会被自动注入到spring中

````

## 注册功能

### 1.配置application.properties文件

````yml
community.path.domain: http://localhost:8080
server.servlet.context-path: /community
````

### 2.创建工具类(处理MD5加密、生成随机数、激活标志接口)

```java
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){
if (StringUtils.isBlank(key)){
return null;
}
//MD5加密方法
return DigestUtils.md5DigestAsHex(key.getBytes());
//参数是bytes型
}
}
```

```java
public interface CommuityConstant {
/* 以下用于注册功能 */
/** 激活成功*/
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;
}
```

### 3.编写Service业务层(实现CommuityConstant接口)

#### 3.1注册业务

```java
//..注入userMapper,mailClient,templateEngine
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
//注册功能
/**为什么返回的是Map类型,因为用Map来存各种情况下的信息,返回给前端页面* */
public Map register(User user){
HashMap map = new HashMap<>();
/*
判输入
*/
if (user == null) {
throw new IllegalArgumentException("参数不能为空!");
}
if (StringUtils.isBlank(user.getUsername())){
map.put("usernameMsg","账户不能为空");
}
if (StringUtils.isBlank(user.getPassword())){
map.put("passwordMsg","密码不能为空");
}
if (StringUtils.isBlank(user.getEmail())){
map.put("emailMsg","邮箱不能为空");
}
/*
判存在
*/
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg","该账号已存在!");
return map;
}
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg","该邮箱已被注册!");
return map;
}
/*
注册账户
1.设置salt加密(随机5位数加入密码)
2.设置密码+salt
3.设置随机数激活码
4.设置status,type=0,时间
5.设置头像(动态)
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000))
*/
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));
user.setActivationCode(CommunityUtil.generateUUID());
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
user.setStatus(0);
user.setType(0);
user.setCreateTime(new Date());
userMapper.insertUser(user);
/*
激活邮件
1.创建Context对象-->context.setVariable(name,value)将name传入前端
为thymeleaf提供变量
2.设置email和url
3.templateEngine.process执行相应HTML
4.发送邮件
*/
Context context = new Context();
context.setVariable("email",user.getEmail());
//http://localhost:8080/community/activation/101/code激活链接
String url=domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode();
context.setVariable("url",url);

String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(),"激活账号",content);
return map;
}
```

#### 3.2激活邮件业务

```java
/**激活邮件功能* */
public int activation(int userId,String code){
User user = userMapper.selectById(userId);
if (user.getStatus()==1){
return ACTIVATION_REPEAT;
}else if (user.getActivationCode().equals(code)){
userMapper.updateStatus(userId,1);
return ACTIVATION_SUCCESS;
}else {
return ACTIVATION_FAILURE;
}
}
```

### 4.编写Controller层

```java
//注册Controller
@RequestMapping(value = "/register",method = RequestMethod.POST)
public String register(Model model, User user){
Map map = userService.register(user);
if (map == null || map.isEmpty()){
map.put("msg","注册成功,我们已经向您的邮件发送了一封激活邮件,请尽快激活!");
map.put("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";
}
}
```

```java
/**激活邮件Controller**/
//http://localhost:8080/community/activation/101/code激活链接
@RequestMapping(value = "/activation/{userId}/{code}",method = RequestMethod.GET)
public String activation(Model model, @PathVariable("userId") int userId,@PathVariable("code") String code){
int result = userService.activation(userId, code);
if (result == ACTIVATION_SUCCESS){
model.addAttribute("msg","激活成功,你的账号已经可以正常使用了!");
model.addAttribute("target","/login");
}else if (result == ACTIVATION_REPEAT){
model.addAttribute("msg","无效操作,该账号已经激活过了!");
model.addAttribute("target","/index");
}else {
model.addAttribute("msg","激活失败,你提供的激活码不正确!");
model.addAttribute("target","/index");
}
return "/site/operate-result";
}
```

### 5.编写前端Thymeleaf页面核心点

```html
/**注册页面 */




该账号已存在!

立即注册

/**账号激活中间页* */


激活状态信息



系统会在 8 秒后自动跳转,
您也可以点此 链接, 手动跳转!


$(function(){
setInterval(function(){
var seconds = $("#seconds").text();
$("#seconds").text(--seconds);
if(seconds == 0) {
location.href = $("#target").attr("href");
}
}, 1000);
});

/**邮箱模板页* */


xxx@xxx.com, 您好!



您正在注册xxx, 这是一封激活邮件, 请点击
此链接,
激活您的xxx账号!



```

## 生成验证码

参考网站 :[http://code.google.com/archive/p/kaptcha/](http://code.google.com/archive/p/kaptcha/ "http://code.google.com/archive/p/kaptcha/")

注意:1.Producer是Kaptcha的核心接口 2.DefaultKaptcha是Kaptcha核心接口的默认实现类

3.Spring Boot没有为Kaptcha提供自动配置

### 1.引入pom.xml

```xml

com.github.penggle
kaptcha
2.3.2

```

### 2.创建配置类装配第三方bean

```java
@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", "宋体,楷体,微软雅黑");

DefaultKaptcha Kaptcha=new DefaultKaptcha();
Config config=new Config(properties);
Kaptcha.setConfig(config);

return Kaptcha;
}
}
```

### 3.编写Controller接口

```java
@RequestMapping(value = "/kaptcha",method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session){
//生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//将验证码存入session
session.setAttribute("kaptcha",text);
//将图片输出给浏览器
response.setContentType("image/png");
try {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image,"png",os);
}catch (IOException e){
logger.error("响应验证码失败:"+e.getMessage());
}
}
```

### 4.Thymeleaf前端页面核心点

```html

function refresh_kaptcha() {
var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
$("#kaptchaImage").attr("src", path);
}

var CONTEXT_PATH="/community";
```

## 登录功能

验证账号,密码,验证码(成功:生成登录凭证ticket,发放给客户端 失败:跳转回登录页 )

### 1.创建登录凭证实体类(登录凭证相当于Session的作用)

注意 :**为什么要搞一个登录凭证,因为最好不要将User信息存入Model返回给前端,敏感信息尽量不要返回给浏览器,不安全,而是选择ticket凭证,通过ticket可以在服务器端得到User**

### 2.编写Dao层接口(注解方式实现)

```java
@Insert({
"insert into login_ticket(user_id,ticket,status,expired) ",
"values (#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true,keyProperty = "id")
//登录功能需要添加登录凭证ticket
int insertLoginTicket(LoginTicket loginTicket);

@Select({
"select id,user_id,ticket,status,expired ",
"from login_ticket ",
"where ticket=#{ticket}"
})
//检查登录状态
LoginTicket selectByTicket(String ticket);

/**
* 一定要加@Param()不然会报错
* 退出功能需要修改status状态
* @return error:com.mysql.jdbc.MysqlDataTruncation:Data truncation:Truncated incorrect DOUBLE value:...
*/
@Update({
"update login_ticket set status=#{status} where ticket=#{ticket} "
})
int updateStatus(@Param("ticket") String ticket, @Param("status") int status);
```

### 3.编写Service层登录业务

```java
/**登录功能**/
public Map login(String username,String password,int expiredSeconds){
HashMap map = new HashMap<>();
//空值处理
if(StringUtils.isBlank(username)){
map.put("usernameMsg","号码不能为空!");
return map;
}
if(StringUtils.isBlank(password)){
map.put("passwordMsg","密码不能为空!");
return map;
}
//验证账号
User user = userMapper.selectByName(username);
if (user==null){
map.put("usernameMsg","该账号不存在!");
return map;
}
//验证激活状态
if (user.getStatus()==0){
map.put("usernameMsg","该账号未激活!");
return map;
}
//验证密码(先加密再对比)
password=CommunityUtil.md5(password+user.getSalt());
if (!user.getPassword().equals(password)){
map.put("passwordMsg","密码输入错误!");
return map;
}
//生成登录凭证(相当于记住我这个功能==session)
LoginTicket ticket = new LoginTicket();
ticket.setUserId(user.getId());
ticket.setTicket(CommunityUtil.generateUUID());
ticket.setStatus(0);
//当前时间的毫秒数+过期时间毫秒数
ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
loginTicketMapper.insertLoginTicket(ticket);

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

return map;
}
```

### 4.编写Controller层

```java
/**
* 登录功能
* @param username
* @param password
* @param code 用于校验验证码
* @param rememberme 记住我(登录凭证)
* @param model 用于将数据传递给前端页面
* @param session 用于获取kaptcha验证码
* @param response 用于浏览器接受cookie
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
/**注意username,password这些没有封装进model* */
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 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";
}
}
```

### 5.编写前端Thymeleaf页面核心点

```html



th:value="${param.username}"
id="username" name="username" placeholder="请输入您的账号!" required>

该账号不存在!




记住我
忘记密码?

```

## 退出登录功能

将登录凭证loginTicket中的status置为无效

### 1.编写Service层

```java
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);//来源于LoginTicket的Dao层
}
```

### 2.编写Controller层

```java
/**
* 退出登录功能
* @CookieValue()注解:将浏览器中的Cookie值传给参数
*/
@RequestMapping(value = "/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
return "redirect:/login";//重定向
}
```

## 显示登录信息

涉及到 :****拦截器,多线程****

![](image/1_b7J4nGtYHK.PNG)

### 拦截器Demo示例

注意:

1. 拦截器需实现HandlerInterceptor接口而配置类需实现WebMvcConfigurer接口。

2. preHandle方法在Controller之前执行,若返回false,则终止执行后续的请求。

3. postHandle方法在Controller之后、模板页面之前执行。

4. afterCompletion方法在模板之后执行。

5. 通过addInterceptors方法对拦截器进行配置

**1.创建拦截器类,实现****HandlerInterceptor****接口**

```java
@Component
public class DemoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle:在Controller之前执行");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion:在模板之后执行");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle:在Controller之后,前端模板引擎页面渲染之前执行");
}
}
```

**2.创建拦截器配置类,实现****WebMvcConfigurer****接口**

```java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)
.excludePathPatterns("/ **/ *.css","/* */*.js","/**/ *.png","/* */*.jpg","/ **/ *.jpeg")
.addPathPatterns("/register","/login");
}
```

### 1.首先创建两个工具类降低耦合(Request获取Cookie工具类,获取凭证ticket多线程工具类)

注意:1.ThreadLocal采用**线程隔离**的方式存放数据,可以避免多线程之间出现数据访问冲突。

2.ThreadLocal提供**set**方法,能够以当前线程为key存放数据。**get**方法,能够以当前线程为key获取数据。

3.ThreadLocal提供**remove**方法,能够以当前线程为key删除数据。

```java
public class CookieUtil {
public static String getValue(HttpServletRequest request,String name){
if (request==null||name==null){
throw new IllegalArgumentException("参数为空!");
}
Cookie[] cookies = request.getCookies();
if (cookies!=null){
for (Cookie cookie : cookies){
if (cookie.getName().equals(name)){
return cookie.getValue();
}
}
}
return null;
}
}
```

```java
@Component //放入容器里不用设为静态方法
public class HostHolder {
//key就是线程对象,值为线程的变量副本
private ThreadLocal users = new ThreadLocal<>();
/**以线程为key存入User* */
public void setUser(User user){
users.set(user);
}
/**从ThreadLocal线程中取出User* */
public User getUser(){
return users.get();
}
/**释放线程* */
public void clear(){
users.remove();
}
}
```

### 2.编写Service层

```java
/**通过Cookie=ticket获取登录用户* */
public LoginTicket getLoginTicket(String ticket){
return loginTicketMapper.selectByTicket(ticket);
}
```

### 3.创建登录凭证拦截器类(等同于Controller类)

```java
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

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

@Override
/**在Controller访问所有路径之前获取凭证* */
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**从浏览器Cookie中获取凭证* */
String ticket=CookieUtil.getValue(request,"ticket");

if (ticket!=null){
//查询凭证
LoginTicket loginTicket = userService.getLoginTicket(ticket);
//检查凭证是否有效(after:当前时间之后)
if (loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){
//根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
/**在本次请求中持有用户
* 类似于存入Map,只是考虑到多线程
*/
hostHolder.setUser(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();
}
}
```

### 4.编写拦截器配置类

```java
@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");
}}
```

### 5.前端页面核心点修改

th:if="\${loginUser!=null}" **存在凭证显示\

  • ,不存在则不显示**

    ```html


  • ```

    ## 拦截未登录页面的路径访问(自定义拦截器注解)

    常用的元注解: **@Target:注解作用目标(方法or类) @Retention:注解作用时间(运行时or编译时) @Document:注解是否可以生成到文档里 @Inherited**:**注解继承该类的子类将自动使用@Inherited修饰**

    注意: **若有2个拦截器,拦截器执行顺序为注册在WebMvcConfig配置类中的顺序**

    ### 1.自定义拦截方法类注解(annotation包)并加在需要拦截的方法上

    ```java
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    /**
    * 标记未登录时要拦截的路径访问方法
    */
    public @interface LoginRequired {
    }
    /**加在需要拦截的方法**/
    @LoginRequired
    ```

    ### 2.编写拦截器类实现HandlerInterceptor父类

    ```java
    @Autowired
    //注入hostHolder工具类获取当前状态登录用户
    private HostHolder hostHolder;

    @Override
    /**在请求路径前执行该方法* */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断拦截的目标是不是一个方法
    if (handler instanceof HandlerMethod){
    //如果是一个方法,将handler转化我HandlerMethod类型
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    //获取方法上的自定义注解
    LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
    /**
    * 如果没有登录并且有自定义注解(需要登录才能访问的方法注解)
    * 通过response来重定向,这里不可以通过return 重定向
    */
    if (hostHolder.getUser()==null&&loginRequired!=null){
    response.sendRedirect(request.getContextPath() + "/login");
    return false;
    }
    }
    return true;
    }
    ```

    ### 3.注册进拦截器配置类WebMvcConfig

    ```java
    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

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

    ## 修改密码

    ### 1.编写Dao层

    ```java
    int updatePassword(@Param("id") int id,@Param("password")String password);

    update user set password=#{password} where id=#{id}

    ```

    ### 2.编写Service层

    ```java
    /**修改密码**/
    public Map updatePassword(int userId,String oldPassword,String newPassword){
    HashMap map = new HashMap<>();

    // 空值处理
    if (StringUtils.isBlank(oldPassword)) {
    map.put("oldPasswordMsg", "原密码不能为空!");
    return map;
    }
    if (StringUtils.isBlank(newPassword)) {
    map.put("newPasswordMsg", "新密码不能为空!");
    return map;
    }

    // 验证原始密码
    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());
    userMapper.updatePassword(userId,newPassword);

    return map;
    }
    ```

    ### 3.编写Controller层

    ```java
    /**修改密码 **/
    @RequestMapping(value = "/updatePassword",method = RequestMethod.POST)
    public String updatePassword(String oldPassword, String newPassword, Model model){
    User user = hostHolder.getUser();
    Map map = userService.updatePassword(user.getId(), oldPassword, newPassword);
    if (map == null || map.isEmpty()){
    /**如果更改密码成功,退出登录,并跳到登录页面 **/
    return "redirect:/logout";
    }else{
    model.addAttribute("oldPasswordMsg",map.get("oldPasswordMsg"));
    model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
    return "/site/setting";
    }
    }
    ```

    ## 忘记密码

    ### 1.编写Service层

    ```Java
    // 判断邮箱是否已注册
    public boolean isEmailExist(String email) {
    User user = userMapper.selectByEmail(email);
    return user != null;
    }

    /**
    * 重置忘记密码
    */
    public Map resetPassword(String email, String password) {
    HashMap map = new HashMap<>();

    //空值处理
    if (StringUtils.isBlank(email)) {
    map.put("emailMsg", "邮箱不能为空!");
    return map;
    }
    if (StringUtils.isBlank(password)) {
    map.put("passwordMsg", "密码不能为空!");
    return map;
    }

    //根据邮箱查找用户
    User user = userMapper.selectByEmail(email);
    if (user == null) {
    map.put("emailMsg", "该邮箱尚未注册!");
    return map;
    }

    //重置密码
    password = CommunityUtil.md5(password + user.getSalt());
    userMapper.updatePassword(user.getId(), password);
    // 清理缓存
    clearCache(user.getId());

    //注意这里!
    map.put("user", user);

    return map;
    }
    ```

    ### 2.编写Controller层

    ```Java
    /**
    * 忘记密码页面
    */
    @RequestMapping(path = "/forget", method = RequestMethod.GET)
    public String getForgetPage() {
    return "/site/forget";
    }

    /**
    * 重置密码
    */
    @RequestMapping(path = "/forget/password", method = RequestMethod.POST)
    public String resetPassword(String email, String verifyCode, String password, Model model, HttpSession session) {
    String code = (String) session.getAttribute(email + "_verifyCode");

    if (StringUtils.isBlank(verifyCode) || StringUtils.isBlank(code) || !code.equalsIgnoreCase(verifyCode)) {
    model.addAttribute("codeMsg", "验证码错误!");
    return "/site/forget";
    }

    Map map = userService.resetPassword(email, password);
    if (map.containsKey("user")) {
    return "redirect:/login";
    } else {
    model.addAttribute("emailMsg", map.get("emailMsg"));
    model.addAttribute("passwordMsg", map.get("passwordMsg"));
    return "/site/forget";
    }
    }
    ```

    ### 3.编写前端核心部分

    ```HTML


    邮箱:



    **

    该邮箱已被注册!




    验证码:


    **

    验证码不正确!





    新密码:


    **

    密码长度不能小于8位!



    重置密码

    ```

    # 优化登录功能(使用Redis)

    ## 使用Redis存储验证码

    ### 1.编写RedisUtil工具类设置验证码key值

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

    ### 2.优化LoginController验证码相关代码(优化前是存在session中的)

    ```java
    @Autowired
    private RedisTemplate redisTemplate;
    /**
    * 验证码功能 (Redis优化)
    * @param response
    */
    @RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response) {
    //生成验证码
    String text = kaptchaProducer.createText();
    BufferedImage image = kaptchaProducer.createImage(text);
    //优化前:将验证码存入session.....

    //优化后:生成验证码的归属传给浏览器Cookie
    String kaptchaOwner = CommunityUtil.generateUUID();
    Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
    cookie.setMaxAge(60);
    cookie.setPath(contextPath);
    response.addCookie(cookie);

    //优化后:将验证码存入Redis
    String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    redisTemplate.opsForValue().set(redisKey, text, 60 , TimeUnit.SECONDS);

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

    ```java
    /**
    * 登录功能
    * @param redisKey 用于获取kaptcha验证码
    * @param @CookieValue用于浏览器接受cookie
    * @return
    */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    /**注意username,password这些没有封装进model**/
    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");
    */

    // 优化后:从redis中获取kaptcha的key
    String kaptcha = null;
    // 判断从浏览器传来的Cookie是否为空
    if (StringUtils.isNotBlank(kaptchaOwner)) {
    String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    // 获取key为验证码的redis数据
    kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
    }

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

    ## 使用Redis存存登录凭证

    ### 1.编写RedisUtil工具类设置登录凭证key值

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

    ### 2.优化UserService中LoginTicket相关代码(废弃LoginTicket数据库表,使用redis)

    ```java
    @Autowired
    private RedisTemplate redisTemplate;
    /**
    * 登录功能(redis优化)
    */
    public Map login(String username, String password, int expiredSeconds) {
    HashMap map = new HashMap<>();
    //空值处理
    if (StringUtils.isBlank(username)) {
    map.put("usernameMsg", "号码不能为空!");
    return map;
    }
    if (StringUtils.isBlank(password)) {
    map.put("passwordMsg", "密码不能为空!");
    return map;
    }
    //验证账号
    User user = userMapper.selectByName(username);
    if (user == null) {
    map.put("usernameMsg", "该账号不存在!");
    return map;
    }
    //验证激活状态
    if (user.getStatus() == 0) {
    map.put("usernameMsg", "该账号未激活!");
    return map;
    }
    //验证密码(先加密再对比)
    password = CommunityUtil.md5(password + user.getSalt());
    if (!user.getPassword().equals(password)) {
    map.put("passwordMsg", "密码输入错误!");
    return map;
    }
    //生成登录凭证(相当于记住我这个功能==session)
    LoginTicket ticket = new LoginTicket();
    ticket.setUserId(user.getId());
    ticket.setTicket(CommunityUtil.generateUUID());
    ticket.setStatus(0);
    //当前时间的毫秒数+过期时间毫秒数
    ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds* 1000));
    // 优化前:loginTicketMapper.insertLoginTicket(ticket);

    // 优化后:loginticket对象放入redis中
    String redisKey = RedisKeyUtil.getTicketKey(ticket.getTicket());
    // opsForValue将ticket对象序列化为json字符串
    redisTemplate.opsForValue().set(redisKey, ticket);

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

    return map;
    }
    ```

    ```java
    /**
    * 通过Cookie=ticket获取登录用户(redis优化)
    */
    public LoginTicket getLoginTicket(String ticket) {
    //优化前: return loginTicketMapper.selectByTicket(ticket);
    String redisKey = RedisKeyUtil.getTicketKey(ticket);
    return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
    }
    ```

    ## 使用Redis缓存用户信息

    ### 1.编写RedisUtil工具类设置用户缓存key值

    ```java
    // 用户缓存
    private static final String PREFIX_USER = "user";
    /**用户缓存**/
    public static String getUserKey(int userId) {
    return PREFIX_USER + SPLIT + userId;
    }
    ```

    ### 2.优化UserService中findUserById和userMapper.updateXXX方法

    ```java
    /**
    * 因为经常使用这个方法,所以将它用redis缓存优化
    * 若缓存中有访问的用户直接从缓存中取出,否则从数据库查询后加入redis中作为缓存
    */
    public User findUserById(int userId) {
    // return userMapper.selectById(userId);
    // 从redis缓存中取值
    User user = getCache(userId);
    if (user == null) {
    user = initCache(userId);
    }
    return user;
    }

    /**
    * 更新头像
    */
    public int updateHeader(int userId, String headerUrl) {
    /** 同时处理mysql和redis事务的方法,报错回滚* */
    int rows = userMapper.updateHeader(userId, headerUrl);
    clearCache(userId);
    return rows;
    }

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

    # 会话管理(暂时仅有demo)

    ### 1.面试题Cookie和Session的区别?

    1.cookie是存放在浏览器上的,session是存放在服务器上的。

    2.cookie数据不安全,如果考虑到安全应使用session。

    3.session会增加服务端的内存压力,考虑到减轻服务器性能方面,应当使用cookie。

    4.cookie只能存放一对字符串k-v

    ![](image/cookie_ydwZWF6ZCb.PNG)

    ![](image/session_4ZmfBdmJQn.PNG)

    ### 2.Cookie是干嘛的?

    因为Http是无状态的,所以需要用到cookie。通俗说cookie是用来让服务器记住浏览器的。

    ### 3.分布式session共享方案

    1、粘性session:在nginx中提供一致性哈希策略,可以保持用户ip进行hash值计算固定分配到某台服务器上,负载也比较均衡,其问题是假如有一台服务器挂了,session也丢失了。

    2、同步session:当某一台服务器存了session后,同步到其他服务器中,其问题是同步session到其他服务器会对服务器性能产生影响,服务器之间耦合性较强。

    3、共享session:单独搞一台服务器用来存session,其他服务器都向这台服务器获取session,其问题是这台服务器挂了,session就全部丢失。

    4、redis集中管理session(主流方法):redis为内存数据库,读写效率高,并可在集群环境下做高可用。

    ![](image/Session集群2_6m4V-afro7.PNG)

    ### 4.简单API实现

    ```java
    /**
    * Cookie示例(获取Cookie时@CookieValue有点问题!!)
    */
    @RequestMapping(value = "/cookie/set",method = RequestMethod.GET)
    @ResponseBody
    public String setCookie(HttpServletResponse response){
    //cookie存的必须是字符串
    Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
    cookie.setPath("/Community/test");
    cookie.setMaxAge(60*10);
    response.addCookie(cookie);

    return "set cookie!";
    }

    @RequestMapping(value = "/cookie/get",method = RequestMethod.GET)
    @ResponseBody
    public String getCookie(@CookieValue("code") String code){
    System.out.println(code);
    return "get cookie!";
    }

    /**
    * Session示例
    */
    @RequestMapping(value = "/session/set",method = RequestMethod.GET)
    @ResponseBody
    public String setSession(HttpSession session){
    session.setAttribute("id",1);
    session.setAttribute("name","xmy");
    return "set session!";
    }

    @RequestMapping(value = "/session/get",method = RequestMethod.GET)
    @ResponseBody
    public String getSession(HttpSession session){
    System.out.println(session.getAttribute("id"));
    System.out.println(session.getAttribute("name"));
    return "get session!";
    }
    ```

    # 上传头像功能

    注意:1. 必须是Post请求
    2.表单:enctype="multipart/form-data"
    3.参数类型MultipartFile只能封装一个文件

    上传路径可以是本地路径也可以是web路径

    访问路径**必须**是符合HTTP协议的**Web路径**

    ## 1.编写Service和Dao层

    ```java
    //Dao层

    update user set password=#{password} where id=#{id}

    int updateHeader(@Param("id") int id,@Param("headerUrl") String headerUrl);

    //Service层
    /**更换上传头像**/
    public int updateHeader(int userId,String headerUrl){
    return userMapper.updateHeader(userId,headerUrl);
    }
    ```

    ## 2.编程Controller层

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

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

    //community.path.upload = d:/DemoNowcoder/upload
    @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(value = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
    return "/site/setting";
    }

    //上传头像
    @RequestMapping(value = "/upload",method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model){
    //StringUtils.isBlank(headerImage)
    if (headerImage == null){
    model.addAttribute("error","您还没有选择图片!");
    return "/site/setting";
    }
    /*
    * 获得原始文件名字
    * 目的是:生成随机不重复文件名,防止同名文件覆盖
    * 方法:获取.后面的图片类型 加上 随机数
    */
    String filename = headerImage.getOriginalFilename();
    String suffix = filename.substring(filename.lastIndexOf(".") );

    //任何文件都可以上传,根据业务在此加限制
    if (StringUtils.isBlank(suffix)){
    model.addAttribute("error","文件格式不正确!");
    return "/site/setting";
    }

    //生成随机文件名
    filename = CommunityUtil.generateUUID() + suffix;
    //确定文件存放路劲
    File dest = new File(uploadPath + "/" +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";
    }
    ```

    ```java
    //得到服务器图片
    @RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
    /**void:返回给浏览器的是特色的图片类型所以用void**/
    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());
    }
    }
    ```

    ## 3.前端核心页面

    ```html



    选择一张图片

    该账号不存在!

    ```

    # 过滤敏感词

    前缀树 :1.根节点不包含字符,除根节点以外的每个节点,只包含一个字符

    2.从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应字符串

    3.每个节点的所有子节点,包含的字符串不相同

    核心 :1.有一个指针指向前缀树,用以遍历敏感词的每一个字符

    2.有一个指针指向被过滤字符串,用以标识敏感词的开头

    3.有一个指针指向被过滤字符串,用以标识敏感词的结尾

    ![](image/前缀树_nTNaIPnorr.PNG)

    ### 1.过滤敏感词算法

    **在resources创建sensitive-words.txt文敏感词文本**

    ```java
    /**
    * 过滤敏感词工具类
    * 类似于二叉树的算法
    */
    @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();

    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) {
    // 0x2E80~0x9FFF 是东亚文字范围
    return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    // 构造前缀树数据结构
    private class TrieNode {

    // 关键词结束标识
    private boolean isKeywordEnd = false;

    // 子节点(key是下级字符,value是下级节点)
    private Map 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);
    }

    }

    }
    ```

    ### 2.引入第三方Maven,如下:

    [*https://github.com/jinrunheng/sensitive-words-filter* ](https://github.com/jinrunheng/sensitive-words-filter "https://github.com/jinrunheng/sensitive-words-filter")

    ```xml

    io.github.jinrunheng
    sensitive-words-filter
    0.0.1

    ```

    # 发布贴子

    核心 **:ajax异步:整个网页不刷新,访问服务器资源返回结果,实现局部的刷新。**

    实质:**JavaScript**和XML(但目前**JSON**的使用比XML更加普遍)

    封装**Fastjson**工具类

    ```javascript
    //使用fastjson,将JSON对象转为JSON字符串(前提要引入Fastjson)
    public static String getJSONString(int code, String msg, Map 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);
    }
    ```

    ### ajax异步Demo示例

    ```java
    /**
    * 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,"操作成功!");
    }
    ```

    ```javascript
    //异步JS

    function send() {
    $.post(
    "/community/test/ajax",
    {"name":"张三","age":25},
    //回调函数返回结果
    function(data) {
    console.log(typeof (data));
    console.log(data);
    //返回json字符串格式(fastJson)
    data = $.parseJSON(data);
    console.log(typeof (data));
    console.log(data.code);
    console.log(data.msg);
    }
    )
    }

    ```

    ## 1.编写Mapper层

    ```xml
    int insertDiscussPost(DiscussPost discussPost);

    user_id,title,content,type,status,create_time,comment_count,score

    insert into discuss_post()
    values (#{userId}, #{title}, #{content}, #{type}, #{status}, #{createTime}, #{commentCount}, #{score})

    ```

    ## 2.编写Service层

    ```java
    public int addDiscussPost(DiscussPost post){
    if(post == null){
    //不用map直接抛异常
    throw new IllegalArgumentException("参数不能为空!");
    }
    //转义HTML标签,Springboot自带转义工具HtmlUtils.htmlEscape()
    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);
    }

    ```

    ## 3.编写Controller层(异步请求要加@ResponseBody,且不用在Controller层用Model,用Js)

    ```java
    @Autowired
    private DiscussPostService discussPostService;
    @Autowired
    private HostHolder hostHolder;

    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @ResponseBody //返回Json格式,一定要加@ResponseBody
    public String addDiscussPost(String title, String content){
    //获取当前登录的用户
    User user = hostHolder.getUser();
    if (user == null){
    //403权限不够
    return CommunityUtil.getJSONString(403,"你还没有登录哦!");
    }
    DiscussPost post = new DiscussPost();
    post.setUserId(user.getId());
    post.setTitle(title);
    post.setContent(content);
    post.setCreateTime(new Date());
    //业务处理,将用户给的title,content进行处理并添加进数据库
    discussPostService.addDiscussPost(post);

    //返回Json格式字符串给前端JS,报错的情况将来统一处理
    return CommunityUtil.getJSONString(0,"发布成功!");
    }
    ```

    ## 4.编写前端异步JS

    注意:\$.parseJSON(data) →通过jQuery,将服务端返回的JSON格式的字符串转为js对象

    ```javascript
    $(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);
    }
    );
    }
    ```

    # 查看帖子详情

    ## 1.编写Mapper层

    ```xml
    DiscussPost selectDiscussPostById(int id);
    <---------------------->

    select
    from discuss_post
    where id = #{id}

    ```

    ## 2.编写Service层

    ```java
    public DiscussPost findDiscussPostById(int id){
    return discussPostMapper.selectDiscussPostById(id);
    }
    ```

    ## 3.编写Controller层

    ```java
    @RequestMapping(value = "/detail/{discussPostId}", method = RequestMethod.GET)
    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";
    }
    ```

    ## 4.编写前端核心部分(进入详情链接及Controller层中的model)

    ```html


  • 标题链接
  • th:utext="${post.getTitle()}"
    th:src="${user.getHeaderUrl()}"
    th:utext="${user.getUsername()}"
    th:text="${#dates.format(post.getCreateTime(),'yyyy-MM-dd HH:mm:ss')}"
    th:utext="${post.getContent()}"
    ```

    # 事务管理

    ## 1.概念

    ### 1.1事务的特性

    原子性:**即事务是应用中不可再分的最小执行体。**

    一致性:**即事务执行的结果,必须使数据从一个一致性状态,变为另一个一致性状态。**

    隔离性:**即各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。**

    持久性:**事务一旦提交,对数据所做的任何改变都要记录到永久存储器。**

    ### 1.2事务的四种隔离级别

    Read Uncommitted: 读未提交(级别**最低**)

    Read Committed: 读已提交

    Repeatable Read: 可重复读

    Serializable: 串行化(级别**最高** ,*性能最低,因为要加锁)*

    ### 1.3并发异常

    - 第一类丢失更新

    - 第二类丢失更新

    - 脏读

    - 不可重复读

    - 幻读

    ![](image/3_Mbdb-PY0NL.PNG)

    ![](image/4_YlSXH6_OBG.PNG)

    ![](image/5_a3J6VuhqzZ.PNG)

    ![](image/6_4dy3KJ0Wtd.PNG)

    ![](image/7_B6xJyOOtTx.PNG)

    ![](image/8_Nd_jlUfSXc.PNG)

    ![](image/9_hSNFRdQQ1L.PNG)

    ## 2.Spring声明式事务

    方法: **1.通过XML配置 2.通过注解@Transaction,如下:**

    ```java
    /* REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务
    * REQUIRED_NEW: 创建一个新事务,并且暂停当前事务(外部事务)
    * NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会和REQUIRED一样
    * 遇到错误,Sql回滚 (A->B)
    */
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    ```

    ## 3.Spring编程式事务(通常用来管理中间某一小部分事务)

    **方法:** **通过TransactionTemplate组件执行SQL管理事务,如下:**

    ```java
    public Object save2(){
    transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
    transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    return transactionTemplate.execute(new TransactionCallback() {
    @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"; }
    });
    }
    ```

    # 评论功能

    ## 显示评论(评论和评论中的回复)

    ### 1.编写Dao层接口

    ```java
    /**
    * 根据评论类型(帖子评论和回复评论)和评论Id--分页查询评论
    * @return Comment类型集合
    */
    List selectCommentsByEntity(@Param("entityType") int entityType, @Param("entityId") int entityId,
    @Param("offset") int offset, @Param("limit") int limit);

    int selectCountByEntity(@Param("entityType") int entityType, @Param("entityId") int entityId);


    select
    from comment
    where status = 0
    and entity_type = #{entityType}
    and entity_Id = #{entityId}
    order by create_time asc
    limit #{offset}, #{limit}


    select count(id)
    from comment
    where status = 0
    and entity_type = #{entityType}
    and entity_id = #{entityId}


    ```

    ### 2.编写业务Service层

    ```java
    public List findCommentsByEntity(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);
    }
    ```

    ### 3.编写Controller控制层(接查看帖子详情,如上)难点(类似于套娃)!

    ```java
    @RequestMapping(value = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscusspost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
    //通过前端传来的Id查询帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post", post);

    //查询发帖人的头像及用户名
    User user = userService.findUserById(post.getUserId());
    model.addAttribute("user", user);

    // 点赞数量
    long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
    model.addAttribute("likeCount", likeCount);

    // 点赞状态 (没登录就显示0)
    int likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);
    model.addAttribute("likeStatus", likeStatus);

    //设置评论分页信息
    page.setLimit(3);
    page.setPath("/discuss/detail/"+discussPostId);
    page.setRows(post.getCommentCount());

    // 评论: 给帖子的评论
    // 回复: 给评论的评论
    // 评论列表集合
    List commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());

    // 评论VO(viewObject)列表 (将comment,user信息封装到每一个Map,每一个Map再封装到一个List中)
    List> commentVoList = new ArrayList<>();
    if (commentList != null){
    // 每一条评论及该评论的用户封装进map集合
    for (Comment comment : commentList){
    // 评论Map-->commentVo
    HashMap commentVo = new HashMap<>();
    // 评论
    commentVo.put("comment", comment);
    // 作者(由comment表中 entity = 1 查user表)
    commentVo.put("user", userService.findUserById(comment.getUserId()));

    // 点赞数量
    likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
    commentVo.put("likeCount", likeCount);
    // 点赞状态 (没登录就显示0)
    likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
    commentVo.put("likeStatus", likeStatus);

    // 回复列表集合(每一条评论的所有回复,不分页)
    List replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);

    // 回复VO
    List> replyVoList = new ArrayList<>();
    if (replyList !=null){
    for (Comment reply : replyList){
    // 回复Map
    HashMap replyVo = new HashMap<>();
    // 回复
    replyVo.put("reply", reply);
    // 作者 (由comment表中 entity = 2 查user表)
    replyVo.put("user", userService.findUserById(reply.getUserId()));
    // 回复目标 (有2种:1.直接回复 2.追加回复)
    User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
    replyVo.put("target", target);

    // 点赞数量
    likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
    replyVo.put("likeCount", likeCount);
    // 点赞状态 (没登录就显示0)
    likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());
    replyVo.put("likeStatus", likeStatus);

    // 将每一个回复Map放在回复List中
    replyVoList.add(replyVo);
    }
    }
    // 将每一个回复List放在评论Map中
    commentVo.put("replys", replyVoList);

    // 回复数量统计
    int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
    commentVo.put("replyCount", replyCount);

    // 再将每一个评论Map放在评论List中
    commentVoList.add(commentVo);
    }
    }
    // 最后将整个List传给前端model渲染
    model.addAttribute("comments", commentVoList);

    return "/site/discuss-detail";
    }
    ```

    ### 4.编写前端Thymeleaf页面(核心部分)

    注意: xxxStat—>Thymeleaf内置对象

    ```html


  • 用户头像

    用户姓名

    1 评论楼层#



    评论内容

    发布于 时间








  • 回复人姓名



    回复人姓名 回复
    被回复人姓名

    回复内容



    回复时间




    回复


  • 最后复用分页:th:replace="index::pagination"
    ```

    ## 添加评论 (用到事务管理)

    ### 1.编写Dao层 (1.增加评论数据CommentMapper 2.修改帖子评论数量DiscussPostMapper)

    ```java
    //CommentMapper
    int insertComment(Comment comment);

    insert into comment()
    values (#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime})


    //DiscussPostMapper
    int updateCommentCount(@Param("id") int id,@Param("commentCount") int commentCount);

    update discuss_post set comment_count = #{commentCount}
    where id = #{id}

    ```

    ### 2.编写业务Service层

    ```java
    //DiscussPostService
    public int updateCommentCount(int id, int commentCount){
    return discussPostMapper.updateCommentCount(id, commentCount);
    }

    //CommentService
    /**
    * 添加评论(涉及事务)
    * 先添加评论,后修改discuss_post中的评论数(作为一个整体事务,出错需要整体回滚!)
    */
    @Transactional(isolation = Isolation.READ_COMMITTED, 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.insertComment(comment);
    /**
    * 更新帖子评论数量
    * 如果是帖子类型才更改帖子评论数量,并且获取帖子评论的id
    */
    if (comment.getEntityType() == ENTITY_TYPE_POST){
    int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
    discussPostService.updateCommentCount(comment.getEntityId(), count);
    }
    return rows;
    }
    ```

    ### 3.编写Controller层

    ```java
    //需要从前端带一个参数
    @RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
    public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment){
    comment.setUserId(hostHolder.getUser().getId());
    comment.setStatus(0);
    comment.setCreateTime(new Date());
    commentService.addComment(comment);
    return "redirect:/discuss/detail/" + discussPostId;
    }
    ```

    ### 4.编写Thymleaf前端页面(核心)

    ```html






    回帖







    回复








    回复

    ```

    # 私信功能

    ## 显示私信列表(难度在写SQL)

    ### 1.编写Dao层

    ```java
    /**查询当前用户的会话列表,针对每个会话只返回一条最新的私信**/
    List selectConversations(@Param("userId") int userId,@Param("offset") int offset,@Param("limit") int limit);

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

    /**查询某个会话所包含的私信列表**/
    List selectLetters(@Param("conversationId") String conversationId,@Param("offset") int offset,@Param("limit") int limit);

    /**查询某个会话所包含的私信数量**/
    int selectLetterCount(@Param("conversationId") String conversationId);
    /**
    * 查询未读的数量
    * 1.带参数conversationId :私信未读数量
    * 2.不带参数conversationId :当前登录用户 所有会话未读数量
    */
    int selectLetterUnreadCount(@Param("userId")int userId,@Param("conversationId") String conversationId);
    ```

    ### 2.编写Mapper.xml(难度)

    ```sql

    id, from_id, to_id, conversation_id, content, status, create_time

    select
    from message
    where id in (
    //子句根据id大小查与每个用户最新的私信(同一会话id越大,私信越新)
    //也可根据时间戳判断
    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 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
    from message
    where status != 2
    and from_id != 1
    and conversation_id = #{conversationId}
    order by id asc
    limit #{offset}, #{limit}

    select count(id)
    from message
    where status != 2
    and from_id != 1
    and conversation_id = #{conversationId}

    select count(id)
    from message
    where status = 0
    and from_id != 1
    and to_id = #{userId}
    //=null:所有会话未读数 !=null:每条会话未读数
    and conversation_id = #{conversationId}

    ```

    ### 3.编写Service层

    ```java
    @Autowired
    private MessageMapper messageMapper;

    public List findConversations(int userId, int offset, int limit){
    return messageMapper.selectConversations(userId, offset, limit);
    }
    public int findConversationCount(int userId) {
    return messageMapper.selectConversationCount(userId);
    }
    public List findLetters(String conversationId, int offset, int limit) {
    return messageMapper.selectLetters(conversationId, offset, limit);
    }
    public int findLetterCount(String conversationId) {
    return messageMapper.selectLetterCount(conversationId);
    }
    public int findLetterUnreadCount(int userId, String conversationId) {
    return messageMapper.selectLetterUnreadCount(userId, conversationId);
    }
    ```

    ### 4.编写Controller层

    #### 4.1私信列表Controller

    ```java
    /**私信列表**/
    @RequestMapping(value = "/letter/list", method = RequestMethod.GET)
    public String getLetterList(Model model, Page page){
    // 获取当前登录用户
    User user = hostHolder.getUser();
    // 分页信息
    page.setLimit(5);
    page.setPath("/letter/list");
    page.setRows(messageService.findConversationCount(user.getId()));
    // 会话列表
    List conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());
    List> conversations = new ArrayList<>();
    if (conversationList != null){
    for (Message message : conversationList){
    HashMap map = new HashMap<>();
    // 与当前登录用户每一条会话的所有信息
    map.put("conversation", message);
    // 当前登录用户与每一个会话人的私信条数
    map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
    // 当前登录用户与每一个会话人的未读条数
    map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
    // 当前登录用户若与当前会话信息中fromId相同,则目标id为ToId;
    int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
    User target = userService.findUserById(targetId);
    map.put("target", target);

    conversations.add(map);
    }
    }
    model.addAttribute("conversations", conversations);
    // 当前登录用户总未读条数
    int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
    model.addAttribute("letterUnreadCount", letterUnreadCount);

    return "/site/letter";
    }
    ```

    #### 4.2私信详情Controller

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

    //获取私信信息
    List letterlist = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
    List> letters = new ArrayList<>();
    if (letterlist != null){
    for(Message message : letterlist){
    HashMap map = new HashMap<>();
    //map封装每条私信
    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";
    }

    /**封装获取目标会话用户(将如:101_107拆开) **/
    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);
    }
    }

    ```

    ### 5.编写Thymeleaf前端页面(核心)

    #### 5.1私信列表页面

    ```html

    朋友私信总私信未读数


  • 单个会话未读数

    用户头像


    会话目标姓名
    会话最新时间
    会话内容,可进入详情页



  • ```

    #### 5.2私信详情页面

    ```html

    来自 目标会话用户 的私信


  • 用户头像

    会话发起人姓名
    时间


    私信内容

  • ```

    ## 发送私信功能(异步)

    ### 1.编写Dao层

    ```sql
    /**插入会话**/
    int insertMessage(Message message);
    /**批量更改每个会话的所有未读消息为已读**/
    int updateStatus(@Param("id") List ids,@Param("status") int status);

    -----------------------Mapper.xml-----------------------------

    insert into message()
    values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})



    update message set status = #{status}
    where id in
    -----批量传入id写法

    #{id}



    ```

    ### 2.编写Service层

    ```java
    public int addMessage(Message message){
    //转义标签
    message.setContent(HtmlUtils.htmlEscape(message.getContent()));
    //过滤敏感词
    message.setContent(sensitiveFilter.filter(message.getContent()));
    return messageMapper.insertMessage(message);
    }
    public int readMessage(List ids){
    return messageMapper.updateStatus(ids, 1);
    }
    ```

    ### 3.编写Controller层

    #### 3.1设置已读

    ```java
    @RequestMapping(value = "/letter/detail/{conversationId}", method = RequestMethod.GET)
    public String getLetterDetail(@PathVariable("conversationId")String conversationId, Model model, Page page){
    /**
    * 以上省略。。。。。。
    */
    //设置已读(当打开这个页面是就更改status =1)
    List ids = getLetterIds(letterlist);
    if (!ids.isEmpty()) {
    messageService.readMessage(ids);
    }
    }

    /**获得批量私信的未读数id* */
    private List getLetterIds(List letterList){
    List ids = new ArrayList<>();

    if (letterList != null) {
    for (Message message : letterList) {
    //只有当前登录用户与message列表中目标用户一致并且staus = 0 时才是未读数,加入未读私信集合
    if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
    ids.add(message.getId());
    }
    }
    }
    return ids;
    }
    ```

    #### 3.2 发送私信

    ```java
    /**发送私信* */
    @RequestMapping(value = "/letter/send", method = RequestMethod.POST)
    @ResponseBody
    public String sendLetter(String toName, String content){
    //根据目标发送人姓名获取其id
    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);
    }
    ```

    ### 4.编写前端JS异步请求(ajax)

    ```javascript
    function send_letter() {
    $("#sendModal").modal("hide");
    //若用JS异步请求,前端参数不用name= "xxx",用如下方法
    var toName = $("#recipient-name").val();
    var content = $("#message-text").val();

    $.post(
    // 接口路径(与@RequestMapping(value = "/letter/send", method = RequestMethod.POST)路径一致)
    CONTEXT_PATH + "/letter/send",
    // 接口参数(与public String sendLetter(String toName, String content)参数一致)
    {"toName":toName, "content":content},
    function (data) {
    // 把{"toName":toName, "content":content}转换成JS对象
    data = $.parseJSON(data);
    // 与CommunityUtil.getJSONString(0,"msg")匹配--0:成功
    if (data.code == 0){
    $("#hintBody").text("发送成功!");
    }else {
    $("#hintBody").text(data.msg);
    }

    $("#hintModal").modal("show");
    setTimeout(function(){
    $("#hintModal").modal("hide");
    //刷新页面
    location.reload();
    }, 2000);
    }
    );
    }
    ```

    # 点赞功能(Redis+异步ajax)

    ## 点赞、取消点赞

    注意:**1引入pom,配置Yaml**

    2.因为访问的是Redis,无需编写Dao层

    ### 1.创建RedisKeyUtil工具类(统一格式化redis的key)

    k:v = like:entity:entityType:entityId -> set(userId)

    ```java
    private static final String SPLIT = ":";
    private static final String PREFIX_ENTITY_LIKE = "like:entity";
    private static final String PREFIX_USER_LIKE = "like:user";
    /**
    * 某个实体的赞
    * key= like:entity:entityType:entityId -> value= userId
    */
    public static String getEntityLikeKey(int entityType, int entityId){
    return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
    }
    ```

    ### 2.直接编写Service业务层

    ```java
    @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从中移除
    red