SSM框架Demo: 简朴博客系统


简朴博客系统:简单朴素…

1. 前端页面效果

♨️注册页

包含以下用户信息:

  1. 用户名
  2. 密码

img

♨️登录页

包含以下用户信息:

  1. 用户名
  2. 密码

img

♨️文章详情页

登录状态下“登录”按钮变为“注销”按钮。

包含以下用户信息:

  1. 博文作者 id
  2. 代码仓库链接
  3. 文章总数

包含以下博文信息:

  1. 作者 id
  2. 文章 id
  3. 标题
  4. 时间
  5. 正文
  6. 阅读量

img

♨️个人博客列表页

包含以下博文信息:

  1. 标题
  2. 时间
  3. 摘要

img

♨️文章汇总列表页

登录状态下“登录”按钮变为“注销”按钮。

包含以下博文信息:

  1. 标题
  2. 时间
  3. 摘要

img

♨️博客编辑页

包含以下用户信息:

  1. 用户id

包含以下博文信息:

  1. 作者 id,即当前用户 id
  2. 标题
  3. 正文
  4. 创建时间,即提交时的时间
  5. 自动生成的文章 id

img

2. 项目创建

使用 Spring 全家桶 + MyBatis 框架进行开发。

img

创建项目目录:

controller,前后端交互控制器,接收请求,处理请求,调用 service,将响应返回给前端。

service,调用数据持久层 dao 层。

dao,进行数据库操作。

model,实体类。

common,公共类,Utils 工具类。

config,配置类。

img

3. 前期配置

当我们创建完一个 Spring 项目之后我们首先就是要准备好相关的配置文件以及创建好数据库。

3.1. 创建数据库数据表

创建数据库

-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
 
-- 选中数据库
use mycnblog;

包含以下两张表:

  1. userinfo 用户表
  2. articleinfo 文章表

🎯userinfo

  1. id,用户 id(主键)
  2. username,用户名
  3. password,密码
  4. photo,头像
  5. createtime,创建时间
  6. updatetime,更新时间
  7. state 状态(预留字段)
-- 创建表用户表
drop table if exists  userinfo;
create table userinfo(
    id int primary key auto_increment,
    username varchar(100) not null unique,
    password varchar(100) not null,
    photo varchar(500) default '',
    createtime datetime default now(),
    updatetime datetime default now(),
    `state` int default 1
) default charset 'utf8mb4';

🎯articleinfo

  1. id,文章 id(自增主键)
  2. title,标题
  3. content,正文
  4. createtime,创建时间
  5. updatetime,更新时间
  6. uid,用户 id
  7. rcount,阅读量
  8. state 状态(预留字段)
-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime datetime default now(),
    updatetime datetime default now(),
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';

初始数据:

-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES 
(1, 'admin', 'admin', '', '2023-11-06 17:10:48', '2023-11-06 17:10:48', 1);
 
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
    values('Java','Java正文',1);

3.2. 配置文件

# 配置数据库的连接字符串
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog2023?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=111111
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置打印 MyBatis 执行的 SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging.level.com.example.demo=debug
# 设置时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm
spring.jackson.time-zone=GMT+8
# session 过期时间
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session

接着将前端文件导入static中。

获取链接:链接

img

此时就项目就创建完成并且连接上 Mysql 数据库了,接下来就是去实现相关的代码了。

img

4. 创建实体类

🍂model.UserInfo类,对应着数据库中的 userinfo 这张表。

package com.example.demo.model;

import lombok.Data;

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

import org.springframework.util.DigestUtils;

@Data
public class Userinfo implements Serializable {
    private int id;
    private String username;
    private String password;
    private String photo;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private int state;

}

🍂model.ArticleInfo类,对应着数据库中的 articleInfo 这张表。

package com.example.demo.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;

@Data
public class Articleinfo implements Serializable {
    private int id;
    private String title;
    private String content;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private int uid;
    private int rcount; // 文章阅读量
    private int state;
}

🍂model.vo.UserInfoVO扩展类,对于一些特殊情况,特殊处理,可以在这里面增加属性,不是增加在原类里,而数据库的表是没有变化的。

package com.example.demo.model.vo;

import com.example.demo.model.Userinfo;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * userinfo 扩展类
 */
@Data
public class UserinfoVO extends Userinfo {
    private String checkCode;
    private int artCount; // 用户文章数
}

5. 统一处理

统一处理可以让代码更加低耦合,高内聚,更符合单一设计原则。

5.1. 统一返回格式处理

🍂统一格式类 common.ResultAjax

该类实现了Serializable接口,实现了这个接口表示该类的对象可以通过序列化机制转换为字节流,并且可以在网络上传输、存储到文件中或在不同的 Java 虚拟机之间进行传递;序列化是将对象转换为字节序列的过程,反序列化则是将字节序列转换回对象的过程。

  1. 该类包括我们的状态码,状态码的描述信息,以及返回的数据。
  2. 该类重载了一些静态的返回方法分别表示我们返回成功或者返回失败的情况。
package com.example.demo.common;

import lombok.Data;

/**
 * 前后端交互的统一数据格式对象
 */
@Data
public class ResultAjax {
    private int code; // 状态码
    private String msg; // 状态描述信息
    private Object data; // 交互数据

    // 成功
    public static ResultAjax succ(Object data) {
        ResultAjax result = new ResultAjax();
        result.setCode(200);
        result.setMsg("");
        result.setData(data);
        return result;
    }

    public static ResultAjax succ(int code, String msg, Object data) {
        ResultAjax result = new ResultAjax();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    // 失败
    public static ResultAjax fail(int code, String msg) {
        ResultAjax result = new ResultAjax();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }

    public static ResultAjax fail(int code, String msg, Object data) {
        ResultAjax result = new ResultAjax();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

}

🍂统一返回处理器 common.ResponseAdvice

准备好了统一返回的类,为了以防止返回的数据不是规定格式(百密一疏的缺漏情况),可以写一个保底处理类,而正常写的时候,返回数据都是自己调用ResultAjax类的包装方法。

  1. 该类实现了ResponseBodyAdvice接口允许在返回数据之前对返回的数据进行校验和修改。
  2. 需要对String类型的数据进行了特殊的处理,String 类型不同与我们一般的类型,需要注入ObjectMapper对象手动将 String 类型转换成 json 格式。
package com.example.demo.common;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 保底统一返回值处理
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof ResultAjax) {
            return body;
        }
        if (body instanceof String) {
            ResultAjax resultAjax = ResultAjax.succ(body);
            try {
                return objectMapper.writeValueAsString(resultAjax);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return ResultAjax.succ(body);
    }
}

5.2. 统一异常处理

🍂common.ExceptionAdvice

package com.example.demo.common;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 统一异常处理
 */
@RestControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public ResultAjax doException(Exception e) {
        return ResultAjax.fail(-1, e.getMessage());
    }
}

6. 全局变量

🍂common.AppVariable

此类存放我们的全局变量,只放了一个固定的session key

package com.example.demo.common;

/**
 * 全局变量
 */
public class AppVariable {

    // 用户 session key
    public static final String SESSION_USERINFO_KEY = "SESSION_USERINFO";

}

7. Session工具类

🍂common.SessionUtils

该类主要是判断服务器是否存储了用户的Session,然后将此 Session 中的 userinfo 给取出来然后返回;这里用的是一个静态的方法这就方便我们的调用了,用的时候不需要去注入或者 new 了。

package com.example.demo.common;

import com.example.demo.model.Userinfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * session 工具类
 */
public class SessionUtils {

    /**
     * 得到当前登录用户
     */
    public static Userinfo getUser(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null &&
                session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {
            // 登录状态
            return (Userinfo) session.getAttribute(AppVariable.SESSION_USERINFO_KEY);
        }
        return null;
    }

}

8. 登录拦截器

在进入一个页面的时候可能需要用户的登录权限,所以应该对请求做一个拦截,对权限进行校验。

拦截处理如下:

我们首先需要获取当前的 Session,然后判断有没有存储指定的 Session,如果存在的话那就返回 true 意味着继续执行后续的代码,如果不是那就直接跳转到登录的页面,后续的代码自然也就不执行了返回 false。

🍂拦截器 config.LoginIntercept

package com.example.demo.config;

import com.example.demo.common.AppVariable;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 用户拦截器
 */
public class LoginIntercept implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {
            // 用户已登录
            return true;
        }
        // 登录页面
        response.sendRedirect("/login.html");
        return false;
    }
}

🍂拦截规则配置类 config.AppConfig

拦截并不是对所有的请求都去进行拦截,我们会拦截部分的请求然后同样的也是会放开一些的请求,此类就是用来处理我们需要拦截哪些放开哪些东西,并且我们加入Congiguration的注解会随着框架的启动而生效。

package com.example.demo.config;

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 MyConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercept())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/user/islogin")
                .excludePathPatterns("/art/getlistbypage")
                .excludePathPatterns("/art/detail")
                .excludePathPatterns("/editor.md/**")
                .excludePathPatterns("/art/increment_rcount")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/blog_list.html")
                .excludePathPatterns("/blog_content.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/login.html");
    }
}

9. 密码加盐加密

🍁为什么要加密

如果密码没有进行加密一旦被拖库了是非常的危险的,用户信息特别是密码会全部被别人获取,想要防止密码被人看到就得对密码进行加密然后将加密后的密码存入数据库中去,这样即使数据库的密码被得到了也不能知道原密码是多少。

🍁md5

md5 是一种比较常用的加密方式,它是将任意长度的输入通过一个算法然后生成一个 128 位的的二进制值输出,通常情况下是用 32 位的 16 进制来表示,其特点其加密是不可逆的,即加密之后不能通过加密值推测出原始值。

🍁md5 缺点

md5 的加密虽然是不可逆的,但还是有一个问题是我们每次对同一个密码加密其得到的结果是固定的值,那么如果可以以穷举出所有的字符的话那么就可以推测出所有的密码了,这就是我们的彩虹表,彩虹表里面以类似键值对的方式将原始值以及 md5 的加密值存储起来然后不断的去完善这个彩虹表,这样对于绝大多数的密码我们都可以通过彩虹表来找到的,这就存在一定的风险了。

🍁加盐加密原理

加盐算法可以解决 md5 被暴力破解的问题,在用 md5 算法对密码进行加密的时会给原密码加上一个全球不重复的随机的盐值(UUID),这样即使是同一个密码两次不同的加密在加盐之后生成的加密密码也是不一样的,就大大增加了密码破译的成本,更进一步保证了数据。

🎯后端的盐值拼接约定

  1. 盐值跟原始密码直接拼接后进行md5加密。
  2. 盐值跟生成的加密密码直接以$拼接[salt]$[plus password]保存到数据库。

🎯验密过程

  1. 根据$分隔符获取到 [盐值] 和 [加密密码]。
  2. [盐值] 和 [待验证的密码] 拼接后进行 md5 生成[待验证的加密密码]。
  3. 对比 [正确的加密密码] 和 [待验证的加密密码]。

🍂common.PasswordUtils

package com.example.demo.common;

import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

/**
 * 密码工具类
 */
public class PasswordUtils {

    /**
     * 加盐加密
     */
    public static String encrypt(String password) {
        // 1. 生成盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 2. 将盐值 + 密码进行 md5 加密得到最终密码
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
        // 3. 将盐值和最终密码拼接成字符串 (以$分隔) 进行返回
        return salt + "$" + finalPassword;
    }

    /**
     * 加盐验证
     */
    public static boolean decrypt(String password, String dbPassword) {
        if (!StringUtils.hasLength(password) || !StringUtils.hasLength(dbPassword) ||
                dbPassword.length() != 65) {
            return false;
        }
        // 1. 得到盐值
        String[] dbPasswordArray = dbPassword.split("\\$");
        if (dbPasswordArray.length != 2) {
            return false;
        }
        // 盐值
        String salt = dbPasswordArray[0];
        // 最终正确密码
        String dbFinalPassword = dbPasswordArray[1];
        // 2. 加密待验证的密码
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password)
                .getBytes(StandardCharsets.UTF_8));
        // 3. 对比验证
        if (finalPassword.equals(dbFinalPassword)) {
            return true;
        }
        return false;
    }
}

10. 线程池组件

在容器中注入一个线程池,后续的业务逻辑可以使用线程提高执行效率。

🍂config.ThreadPoolConfig

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 线程池组件
 */
@Configuration
public class ThreadPoolConfig {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10000);
        executor.setThreadNamePrefix("MyThread-");
        executor.initialize();
        return executor;
    }
}

11. dao层

11.1. UserMapper

UserMapper 里面存放着关于 userinfo 表的接口,使用 mybatis 注解的方式实现 sql 的映射。

package com.example.demo.dao;

import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
    // 将注册用户的密码保存到数据库中
    @Insert("insert into userinfo(username,password) values(#{username},#{password})")
//    @Insert("insert into userinfo(username,password,updatetime) values(#{username},#{password},null)")
    int reg(Userinfo userinfo);

    // 根据用户名查询用户对象
    @Select("select * from userinfo where username=#{username}")
    Userinfo getUserByName(@Param("username") String username);

    // 根据用户 id 查询用户对象
    @Select("select * from userinfo where id=#{uid}")
    UserinfoVO getUserById(@Param("uid") int uid);

}

11.2. ArticleMapper

ArticleMapper 里面存放着关于 articleinfo 表的接口,使用 mybatis 注解的方式实现 sql 的映射。

package com.example.demo.dao;

import com.example.demo.model.Articleinfo;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface ArticleMapper {

    // 根据用户 id 查询此用户发表的所有文章
    @Select("select * from articleinfo where uid=#{uid} order by id desc")
    List<Articleinfo> getListByUid(@Param("uid") int uid);

    // 判断文章的归属人+删除文章操作
    @Delete("delete from articleinfo where id=#{aid} and uid=#{uid}")
    int del(@Param("aid") Integer aid, int uid);

    // 添加文章到数据库
    @Insert("insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})")
// @Insert("insert into articleinfo(title,content,uid,updatetime) values(#{title},#{content},#{uid},null)")
    int add(Articleinfo articleinfo);

    // 修改文章中间步骤: 查询自己发表的文章详情
    @Select("select * from articleinfo where id=#{aid} and uid=#{uid}")
    Articleinfo getArticleByIdAndUid(@Param("aid") int aid, @Param("uid") int uid);

    // 修改文章, 并效验归属人
    @Update("update articleinfo set title=#{title},content=#{content} where id=#{id} and uid=#{uid}")
    int update(Articleinfo articleinfo);

    // 根据文章 id 查询文章对象
    @Select("select * from articleinfo where id=#{aid}")
    Articleinfo getDetailById(@Param("aid") int aid);

    // 根据 uid 查询用户发表的总文章数
    @Select("select count(*) from articleinfo where uid=#{uid}")
    int getArtCountByUid(@Param("uid") int uid);

    // 更新文章阅读量
    @Update("update articleinfo set rcount=rcount+1 where id=#{aid}")
    int incrementRCount(@Param("aid") int aid);

    // 查询一页的文章列表
    @Select("select * from articleinfo order by id desc limit #{psize} offset #{offset}")
    public List<Articleinfo> getListByPage(@Param("psize") int psize, @Param("offset") int offset);

    // 查询文章表记录数
    @Select("select count(*) from articleinfo")
    int getCount();

}

12. 服务层service

12.1. UserService

package com.example.demo.service;

import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 将注册用户的密码保存到数据库中
    public int reg(Userinfo userinfo) {
        return userMapper.reg(userinfo);
    }

    // 根据用户名查询用户对象
    public Userinfo getUserByName(String username) {
        return userMapper.getUserByName(username);
    }

    // 根据用户 id 查询用户对象
    public UserinfoVO getUserById(int uid) {
        return userMapper.getUserById(uid);
    }

}

12.2. ArticleService

package com.example.demo.service;

import com.example.demo.dao.ArticleMapper;
import com.example.demo.model.Articleinfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    // 根据用户 id 查询此用户发表的所有文章
    public List<Articleinfo> getListByUid(int uid) {
        return articleMapper.getListByUid(uid);
    }

    // 判断文章的归属人+删除文章操作
    public int del(Integer aid, int uid) {
        return articleMapper.del(aid, uid);
    }

    // 添加文章到数据库
    public int add(Articleinfo articleinfo) {
        return articleMapper.add(articleinfo);
    }

    // 修改文章中间步骤: 查询自己发表的文章详情
    public Articleinfo getArticleByIdAndUid(int aid, int uid) {
        return articleMapper.getArticleByIdAndUid(aid, uid);
    }

    // 修改文章, 并效验归属人
    public int update(Articleinfo articleinfo) {
        return articleMapper.update(articleinfo);
    }

    // 根据文章 id 查询文章对象
    public Articleinfo getDetail(int aid) {
        return articleMapper.getDetailById(aid);
    }

    // 根据 uid 查询用户发表的总文章数
    public int getArtCountByUid(int uid) {
        return articleMapper.getArtCountByUid(uid);
    }

    // 更新文章阅读量
    public int incrementRCount(int aid) {
        return articleMapper.incrementRCount(aid);
    }

    // 查询一页的文章列表
    public List<Articleinfo> getListByPage(int psize, int offset) {
        return articleMapper.getListByPage(psize, offset);
    }

    // 查询文章表记录数
    public int getCount() {
        return articleMapper.getCount();
    }

}

13. 核心—控制层controller

控制层是最为核心的一层,负责各种业务逻辑的处理。

package com.example.demo.controller;

import com.example.demo.common.AppVariable;
import com.example.demo.common.PasswordUtils;
import com.example.demo.common.ResultAjax;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    // ...
}
package com.example.demo.controller;

import com.example.demo.common.ResultAjax;
import com.example.demo.common.SessionUtils;
import com.example.demo.model.Articleinfo;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import com.example.demo.service.ArticleService;
import com.example.demo.service.UserService;
import org.apache.ibatis.annotations.Insert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

@RestController
@RequestMapping("/art")
public class ArticleController {
    @Autowired
    private ArticleService articleService;
    private static final int _DESC_LENGTH = 120; // 文章简介的长度
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Autowired
    private UserService userService;

    // ...
}

13.1. UserController

13.1.1. 注册功能

♨️前后端交互接口

后端:

  1. /user/reg
  2. -1 非法参数 | 返回受影响行数

前端:

  1. POST,json,/user/reg
  2. data:username,password

♨️后端实现

  1. 前端在经过一系列的校验之后会给传过来一组 json 数据。
  2. 我们用 UserinfoVO 接收然后将密码进行加密处理。
  3. 调用数据库然后将数据插入其中。
 /**
 * 注册功能接口
 */
@RequestMapping("/reg")
public ResultAjax reg(UserinfoVO userinfo) {
    // 1. 效验参数
    if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername())
            || !StringUtils.hasLength(userinfo.getPassword())) {
        // 参数异常
        return ResultAjax.fail(-1, "非法参数");
    }
    // 密码加盐
    userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));
    // 2. 请求 service 进行添加操作
    int result = userService.reg(userinfo);
    // 3. 将执行的结果返回给前端
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 将注册用户及密码保存到数据库中
@Insert("insert into userinfo(username,password) values(#{username},#{password})")
int reg(Userinfo userinfo);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/reg")

♨️前端逻辑及事件

  1. 用户名不能全为空,并且上传时空白符会被去除掉。
  2. 密码不能全为空,并且上传时空白符会被去除掉。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册页面</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/reg.css">
    <script src="js/jquery.min.js"></script>
</head>

<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">博客广场</a>
    <a href="login.html">登录</a>
    <!-- <a href="#">注销</a> -->
</div>
<!-- 版心 -->
<div class="register-container">
    <!-- 中间的注册框 -->
    <div class="register-dialog">
        <h3>注册</h3>
        <div class="row">
            <span>用户名</span>
            <input type="text" id="username">
        </div>
        <div class="row">
            <span>密码</span>
            <input type="password" id="password">
        </div>
        <div class="row">
            <span>确认密码</span>
            <input type="password" id="password2">
        </div>
        <div class="row">
            <button id="submit" onclick="mysub()">注册</button>
        </div>
        <a href="login.html" class="login">已有账户?登录</a>
    </div>
</div>
<script>
    // 提交用户注册信息
    function mysub() {
        // 1. 参数效验 (获取到数据和非空效验)
        var username = jQuery("#username");
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        if (username.val().trim() == "") {
            alert("请先输入用户名! ");
            username.focus();
            return false;
        }
        if (password.val().trim() == "") {
            alert("请先输入密码! ");
            password.focus();
            return false;
        }
        if (password2.val().trim() == "") {
            alert("请先输入确认密码! ");
            password2.focus();
            return false;
        }
        // 效验两次输入的密码是否一致
        if (password.val() != password2.val()) {
            alert("两次密码不一致, 请先检查! ");
            return false;
        }
        // 2. 将数据提交给后端
        jQuery.ajax({
            url: "/user/reg",
            type: "POST",
            data: {
                "username": username.val().trim(),
                "password": password.val().trim()
            },
            success: function (res) {
                // 3. 将后端返回的结果展示给用户
                if (res.code == 200 && res.data == 1) {
                    // 注册成功
                    alert("注册成功, 欢迎加入!");
                    // 调转到登录页
                    location.href = "login.html";
                } else {
                    // 注册失败
                    alert("出错了: 注册失败, 请重新操作! " + res.msg);
                }
            }
        });
    }
</script>
</body>

</html>

13.1.2. 登录功能

♨️前后端交互接口

后端:

  1. /user/login
  2. -1 非法参数 | -2 用户名或密码错误 | 1

前端:

  1. GET,query string,/user/login
  2. data:username,password

♨️后端实现

  1. 首先进行非空校验,判断用户名和密码是否为空。
  2. 使用用户名进行查询,看当前用户信息是否存在,存在拿到加密密码及 UUID
  3. 把拿到的用户信息中的加密密码与待验证密码进行对比。
  4. 验证成功将用户对象存储到 Session 中。
/**
 * 登录功能接口
 */
@RequestMapping("/login")
public ResultAjax login(UserinfoVO userinfoVO, HttpServletRequest request) {
    // 1. 参数效验
    if (userinfoVO == null || !StringUtils.hasLength(userinfoVO.getUsername()) ||
            !StringUtils.hasLength(userinfoVO.getPassword())) {
        // 非法登录
        return ResultAjax.fail(-1, "非法参数!");
    }
    // 2. 根据用户名查询对象
    Userinfo userinfo = userService.getUserByName(userinfoVO.getUsername());
    if (userinfo == null || userinfo.getId() == 0) {
        // 不存在此用户
        return ResultAjax.fail(-2, "用户名或密码错误!");
    }
    // 3. 使用对象中的密码和用户输入的密码进行比较
    // 加盐解密
    if (!PasswordUtils.decrypt(userinfoVO.getPassword(), userinfo.getPassword())) {
        // 密码错误
        return ResultAjax.fail(-2, "用户名或密码错误!");
    }
    // 4. 比较成功之后,将对象存储到 session 中
    HttpSession session = request.getSession();
    session.setAttribute(AppVariable.SESSION_USERINFO_KEY, userinfo);
    // 5. 将结果返回给用户
    return ResultAjax.succ(1);
}

♨️涉及到的 sql 接口

// 根据用户名查询用户对象
@Select("select * from userinfo where username=#{username}")
Userinfo getUserByName(@Param("username") String username);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/login")

♨️前端逻辑及事件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录页面</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/login.css">
    <script src="js/jquery.min.js"></script>
</head>

<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">博客广场</a>
    <a href="reg.html">注册</a>
</div>
<!-- 版心 -->
<div class="login-container">
    <!-- 中间的登录框 -->
    <div class="login-dialog">
        <h3>登录</h3>
        <div class="row">
            <span>用户名</span>
            <input type="text" id="username" placeholder="手机号/邮箱">
        </div>
        <div class="row">
            <span>密码</span>
            <input type="password" id="password">
        </div>
        <div class="row">
            <button id="submit" onclick="doLogin()">登录</button>
        </div>
        <a href="reg.html">注册</a>
    </div>
</div>
<script>
    // 执行登录操作
    function doLogin() {
        // 1. 效验参数
        // 拿到用户和密码两个框的组件
        var username = jQuery("#username");
        var password = jQuery("#password");
        if (username.val().trim() == "") {
            alert("请先输入用户名! ");
            username.focus();
            return false;
        }
        if (password.val().trim() == "") {
            alert("请先输入密码! ");
            password.focus();
            return false;
        }
        // 2. 将数据提交给后端
        jQuery.ajax({
            url: "/user/login",
            type: "GET",
            data: {
                "username": username.val(),
                "password": password.val()
            },
            success: function (res) {
                // 3. 将结果展示给用户
                if (res.code == 200 && res.data == 1) {
                    // 登录成功
                    // alert("恭喜: 登录成功!");
                    // 跳转到我的文章管理页面
                    location.href = "myblog_list.html";
                } else {
                    // 登录失败
                    alert("出错了: 登录失败, 请重新操作! " + res.msg);
                }
            }
        });
    }
</script>
</body>

</html>

13.1.3. 注销功能

♨️前后端交互接口

后端:

  1. /user/logout
  2. 1

前端:

  1. POST,/user/logout

♨️后端实现

直接将用户 Session 给删除即可。

//注销功能
@RequestMapping("/logout")
public AjaxResult logout(HttpSession session) {
    session.removeAttribute(AppVariable.USER_SESSION_KEY);
    return AjaxResult.success(1);
}

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

在个人列表,汇总列表,博客详情页,博客编辑页的导航栏都有该事件的触发按钮。

img

// 注销功能 js
function logout() {
    if (confirm("是否确定注销? ")) {
        // 1. 去后端删除 session 信息
        jQuery.ajax({
            url: "/user/logout",
            type: "POST",
            data: {},
            success: function (res) {
            }
        });
        // 2. 跳转到登录页面
        location.href = "login.html";
    }
}

13.1.4. 判断当前用户是否登录

♨️前后端交互接口

后端:

  1. /user/islogin
  2. true/false

前端:

  1. GET,/user/islogin

♨️后端实现

判断用户 Session 是否存在即可。

/**
 * 判断用户当前是否登录, 用来修改前端组建
 */
@RequestMapping("/islogin")
public ResultAjax islogin(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null && session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {
        return ResultAjax.succ(true);
    }
    return ResultAjax.succ(false);
}

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/islogin")

♨️前端逻辑及事件

后端返回 true,就将导航栏的 “登录” 按钮改为 “注销”。

在汇总列表页和博客详情页登录状态会触发该事件,整合在两个页面初始化的过程中。

img

// 判断用户是否登录,登录状态将 "登录" 按钮改为注销
jQuery.ajax({
    url: "/user/islogin",
    type: "GET",
    data: {

    },
    success: function (res) {
        // 3. 将结果展示给用户
        if (res.code == 200 && res.data == true) {
            // 获取要修改的 <a> 元素
            var linkElement = document.getElementById("logoutLink");

            // 修改 href 属性为 "javascript:logout()"
            linkElement.href = "javascript:logout()";

            // 修改文本内容为 "注销"
            linkElement.textContent = "注销";
        }
    }
});

13.2. ArticleController

13.2.1. 返回当前登录用户的文章列表

♨️前后端交互接口

后端:

  1. /art/mylist
  2. -1 | 返回博客列表信息

前端:

  1. GET,/art/mylist

♨️后端实现

  1. 通过 Session 获取到当前登录用户的 id,根据用户 id 去查询当前用户的所有文章。
  2. 对文章正文部分进行截取,得到摘要。
/**
 * 得到当前登录用户的文章列表
 */
@RequestMapping("/mylist")
public ResultAjax myList(HttpServletRequest request) {
    // 1. 得到当前登录用户
    Userinfo userinfo = SessionUtils.getUser(request);
    if (userinfo == null) {
        return ResultAjax.fail(-1, "当前未登录: 请先注册/登录! ");
    }
    // 2. 根据用户 id 查询此用户发表的所有文章
    List<Articleinfo> list = articleService.getListByUid(userinfo.getId());
    // 处理 list -> 将文章正文变成简介
    if (list != null && list.size() > 0) {
        // 并行处理 list 集合
        list.stream().parallel().forEach((art) -> {
            if (art.getContent().length() > _DESC_LENGTH) {
                // 截取
                art.setContent(art.getContent().substring(0, _DESC_LENGTH));
            }
        });
    }
    // 3. 返回给前端
    return ResultAjax.succ(list);
}

♨️涉及到的 sql 接口

// 根据用户 id 查询此用户发表的所有文章
@Select("select * from articleinfo where uid=#{uid} order by id desc")
List<Articleinfo> getListByUid(@Param("uid") int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 初始化方法
function init() {
    jQuery.ajax({
        url: "/art/mylist",
        type: "GET",
        data: {},
        success: function (res) {
            if (res.code == 200) {
                // 请求成功
                var createHtml = "";
                var artList = res.data;
                if (artList == null || artList.length == 0) {
                    // 未发表文章
                    createHtml += "<h3 style='margin-left:20px;margin-top:20px'>暂无文章, 快去" +
                        "<a href='blog_add.html'>创作</a>吧! </h3>";
                } else {
                    for (var i = 0; i < artList.length; i++) {
                        var art = artList[i];
                        createHtml += '<div class="blog">';
                        createHtml += '<div class="title">' + art.title + '</div>';
                        createHtml += '<div class="date">' + art.createtime + '</div>';
                        createHtml += '<div class="desc">';
                        createHtml += art.content;
                        createHtml += '</div>';
                        createHtml += ' <a href="blog_content.html?aid=' +
                            art.id + '" class="detail">查看全文</a>&nbsp;&nbsp;';
                        createHtml += '<a href="blog_edit.html?aid=' +
                            art.id + '" class="detail">修改</a>&nbsp;&nbsp;';
                        createHtml += ' <a href="javascript:del(' + art.id + ')" class="detail">删除</a>';
                        createHtml += '</div>';
                    }
                }
                jQuery("#artListDiv").html(createHtml);
            } else {
                alert("" + res.msg);
            }
        }
    });
}

13.2.2. 删除文章功能

♨️前后端交互接口

后端:

  1. /art/del
  2. -1 | 受影响行数

前端:

  1. POST,json,/art/del
  2. data:aid(文章id)

♨️后端实现

删除时需要两个参数,一个是文章的 id 一个是当前登录用户的 id,当登录用户 id 和文章所属用户 id 要相同才能删除文章。

/**
 * 删除文章
 */
@RequestMapping("/del")
public ResultAjax del(Integer aid, HttpServletRequest request) {
    // 1. 参数效验
    if (aid == null || aid <= 0) {
        return ResultAjax.fail(-1, "参数错误! ");
    }
    // 2. 得到当前登录的用户
    Userinfo userinfo = SessionUtils.getUser(request);
    if (userinfo == null) {
        return ResultAjax.fail(-1, "当前未登录: 请先注册/登录! ");
    }
    // 3. 判断文章的归属人+删除操作 where id=aid and uid=uid
    int result = articleService.del(aid, userinfo.getId());
    // 4. 将结果返回给前端
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 判断文章的归属人+删除文章操作
@Delete("delete from articleinfo where id=#{aid} and uid=#{uid}")
int del(@Param("aid") Integer aid, int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 根据文章 id 进行删除操作
function del(aid) {
    // 1.参数效验
    if (aid == "" || aid <= 0) {
        alert("参数错误!");
        return false;
    }
    // 2.将数据返回给后端进行删除操作
    jQuery.ajax({
        url: "/art/del",
        type: "POST",
        data: {
            "aid": aid
        },
        success: function (res) {
            // 3.将结果展示给用户
            if (res.code == 200 && res.data == 1) {
                alert("文章删除成功!");
                // 刷新当前页面
                location.href = location.href;
            } else {
                // 删除失败
                alert("出错了: 删除失败, 请重新尝试! " + res.msg);
            }
        }
    });
}

13.2.3. 查看文章详情功能

♨️前后端交互接口

后端:

  1. /art/detail
  2. user.count(文章数)& user(用户) & art(文章)

前端:

  1. GET,quary string,/art/detail
  2. data:aid

♨️后端实现

  1. 根据文章 id 查询文章信息,看文章是否存在,文章存在,执行后续步骤。
  2. 注册根据 uid 查询用户总文章数的任务
  3. 注册根据 uid 查询用户信息的任务
  4. 线程池执行任务
  5. 构造响应数据,并返回
/**
 * 查询文章详情页
 */
@RequestMapping("/detail")
public ResultAjax detail(Integer aid) throws ExecutionException, InterruptedException {
    // 1. 参数效验
    if (aid == null || aid <= 0) {
        return ResultAjax.fail(-1, "非法参数! ");
    }
    // 2. 查询文章详情
    Articleinfo articleinfo = articleService.getDetail(aid);
    if (articleinfo == null || articleinfo.getId() <= 0) {
        return ResultAjax.fail(-1, "非法参数! ");
    }
    // 3 和 4 是多线程同步查询
    // 3. 根据 uid 查询用户的详情
    FutureTask<UserinfoVO> userTask = new FutureTask(() -> {
        return userService.getUserById(articleinfo.getUid());
    });
    taskExecutor.submit(userTask);
    // 4. 根据 uid 查询用户发表的总文章数
    FutureTask<Integer> artCountTask = new FutureTask<>(() -> {
        return articleService.getArtCountByUid(articleinfo.getUid());
    });
    taskExecutor.submit(artCountTask);
    // 5. 组装数据
    UserinfoVO userinfoVO = userTask.get(); // 等待任务 (线程池) 执行完成
    int artCount = artCountTask.get(); // 等待任务 (线程池) 执行完成
    userinfoVO.setArtCount(artCount);
    HashMap<String, Object> result = new HashMap<>();
    result.put("user", userinfoVO);
    result.put("art", articleinfo);
    // 6. 返回结果给前端
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 根据文章 id 查询文章对象
@Select("select * from articleinfo where id=#{aid}")
Articleinfo getDetailById(@Param("aid") int aid);
// 根据用户 id 查询用户对象
@Select("select * from userinfo where id=#{uid}")
UserinfoVO getUserById(@Param("uid") int uid);
// 根据 uid 查询用户发表的总文章数
@Select("select count(*) from articleinfo where uid=#{uid}")
int getArtCountByUid(@Param("uid") int uid);

♨️拦截器配置

  1. 该接口不需要拦截,需要放开 URL。
  2. editor.md 是个目录,要放开整个目录才行,不然页面渲染就出问题了,其他地方不加能渲染是因为是登录状态,但详情页是不需要登录的。
.excludePathPatterns("/art/detail")
.excludePathPatterns("/editor.md/**")

♨️前端逻辑及事件

img

// 获取查询字符串参数值: 根据 key 获取 url 中对应的 value
function getParamValue(key) {
    // 1. 得到当前url的参数部分
    var params = location.search;
    // 2. 去除“?”
    if (params.indexOf("?") >= 0) {
        params = params.substring(1);
        // 3. 根据“&”将参数分割成多个数组
        var paramArray = params.split("&");
        // 4. 循环对比 key, 并返回查询的 value
        if (paramArray.length >= 1) {
            for (var i = 0; i < paramArray.length; i++) {
                // key=value
                var item = paramArray[i].split("=");
                if (item[0] == key) {
                    return item[1];
                }
            }
        }
    }
    return null;
}
<script type="text/javascript">
    var aid = getParamValue("aid");
    var editormd;

    function initEdit(md) {
        editormd = editormd.markdownToHTML("editorDiv", {
            markdown: md, // Also, you can dynamic set Markdown text
            // htmlDecode : true,  // Enable / disable HTML tag encode.
            // htmlDecode : "style,script,iframe",  // Note: If enabled, you should filter some dangerous HTML tags for website security.
        });
    }

    // 初始化页面
    function init() {
        // 1. 效验参数
        if (aid == null || aid <= 0) {
            alert("参数有误! ");
            return false;
        }
        // 2. 请求后端获取数据
        jQuery.ajax({
            url: "/art/detail",
            type: "GET",
            data: {
                "aid": aid
            },
            success: function (res) {
                // 3. 将数据展示到前端
                if (res.code == 200 && res.data != null) {
                    var user = res.data.user;
                    var art = res.data.art;
                    if (user != null) {
                        // 给用户对象设置值
                        if (user.photo != "") {
                            jQuery("#photo").att("src", user.photo);
                        }
                        jQuery("#username").html(user.username);
                        jQuery("#artcount").html(user.artCount); // 用户发布的总文章数
                    } else {
                        alert("出错了: 查询失败,   请重新操作! " + res.msg);
                    }
                    if (art != null) {
                        jQuery("#title").html(art.title);
                        jQuery("#createtime").html(art.createtime);
                        jQuery("#rcount").html(art.rcount); // 阅读量
                        initEdit(art.content);
                    } else {
                        alert("出错了: 查询失败,   请重新操作! " + res.msg);
                    }
                } else {
                    alert("出错了: 查询失败,   请重新操作! " + res.msg);
                }
            }
        });
        init();
    }
}

13.2.4. 更新文章阅读量

♨️前后端接口

后端:

  1. /art/increment_rcount
  2. -1 | 返回受影响行数

前端:

  1. POST,json,/art/increment_rcount
  2. data:aid
**
 * 更新文章阅读量
 */
@RequestMapping("/increment_rcount")
public ResultAjax incrementRCount(Integer aid) {
    // 1. 效验参数
    if (aid == null || aid <= 0) {
        return ResultAjax.fail(-1, "参数有误! ");
    }
    // 2. 更新数据库 update articleinfo set rcount=rcount+1 where aid=#{aid}
    int result = articleService.incrementRCount(aid);
    // 3. 返回结果
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 更新文章阅读量
@Update("update articleinfo set rcount=rcount+1 where id=#{aid}")
int incrementRCount(@Param("aid") int aid);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/art/getlistbypage")

♨️前端逻辑及事件

在查看文章详情页初始化模块中整合。

// 访问量加 1
function incrementRCount() {
    if (aid == null || aid <= 0) {
        return false;
    }
    jQuery.ajax({
        url: "/art/increment_rcount",
        type: "POST",
        data: {
            "aid": aid
        },
        success: function (res) {
        }
    });
}

incrementRCount();

13.2.5. 添加文章

♨️前后端接口

后端:

  1. /art/add
  2. 返回受影响行数

前端:

  1. POST,json,/art/add
  2. data:title,content

♨️后端实现

  1. 通过 Session 得到当前登录用户的 id。
  2. 将用户 id 赋值到文章对象后插入到数据库。
/**
 * 添加文章
 */
@RequestMapping("/add")
public ResultAjax add(Articleinfo articleinfo, HttpServletRequest request) {
    // 1. 效验参数
    if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
            !StringUtils.hasLength(articleinfo.getContent())) {
        return ResultAjax.fail(-1, "非法参数! ");
    }
    // 2. 组装数据
    Userinfo userinfo = SessionUtils.getUser(request);
    if (userinfo == null) {
        return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");
    }
    articleinfo.setUid(userinfo.getId());
    // 3. 将数据入库
    int result = articleService.add(articleinfo);
    // 4. 将结果返回给前端
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 添加文章到数据库
@Insert("insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})")
int add(Articleinfo articleinfo);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>博客添加</title>

    <!-- 引入自己写的样式 -->
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_edit.css">

    <!-- 引入 editor.md 的依赖 -->
    <link rel="stylesheet" href="editor.md/css/editormd.min.css"/>
    <script src="js/jquery.min.js"></script>
    <script src="editor.md/editormd.js"></script>
    <script src="js/logout.js"></script>
</head>

<body>
<!-- 导航栏 -->
<div class="nav">
    <img src="img/logo2.jpg" alt="">
    <span class="title">博客系统</span>
    <!-- 用来占据中间位置 -->
    <span class="spacer"></span>
    <a href="blog_list.html">博客广场</a>
    <a href="javascript:logout()">注销</a>
</div>
<!-- 编辑框容器 -->
<div class="blog-edit-container">
    <!-- 标题编辑区 -->
    <div class="title">
        <input id="title" type="text" placeholder="在这里写下文章标题">
        <button onclick="mysub()">发布文章</button>
    </div>
    <!-- 创建编辑器标签 -->
    <div id="editorDiv">
        <textarea id="editor-markdown" style="display:none;"></textarea>
    </div>
</div>

<script>
    var editor;

    function initEdit(md) {
        // 编辑器设置
        editor = editormd("editorDiv", {
            // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.
            width: "100%",
            // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
            height: "calc(100% - 50px)",
            // 编辑器中的初始内容
            markdown: md,
            // 指定 editor.md 依赖的插件路径
            path: "editor.md/lib/",
            saveHTMLToTextarea: true //
        });
    }

    initEdit("# 在这里写下一篇博客"); // 初始化编译器的值
    // 提交
    function mysub() {
        // 1. 非空效验
        var title = jQuery("#title");
        if (title.val().trim() == "") {
            alert("请先输入标题! ");
            title.focus();
            return false;
        }
        if (editor.getValue() == "") {
            alert("请先输入正文! ");
            return false;
        }
        // 2. 将用户提交的数据传递给后端
        jQuery.ajax({
            url: "/art/add",
            type: "POST",
            data: {
                "title": title.val(),
                "content": editor.getValue()
            },
            success: function (res) {
                // 3. 将后端返回的结果展示给用户
                if (res.code == 200 && res.data == 1) {
                    // 文章添加成功
                    if (confirm("文章添加成功! 是否继续添加文章? ")) {
                        // 刷新当前页面
                        location.href = location.href;
                    } else {
                        // 跳转到个人文章管理页
                        location.href = "myblog_list.html";
                    }
                } else {
                    // 文章添加失败
                    alert("出错了: 发布失败, 请重新操作! " + res.msg);
                }
            }
        });
    }
</script>
</body>

</html>

13.2.6. 修改文章

13.2.6.1. 页面初始化

♨️前后端交互接口

后端:

  1. /art/update_init
  2. -1 | 文章信息

前端:

  1. GET,quary string,/art/update_init
  2. data:aid

♨️后端实现

/**
 * 修改文章中间步骤: 查询自己发表的文章详情
 */
@RequestMapping("/update_init")
public ResultAjax updateInit(Integer aid, HttpServletRequest request) {
    // 1. 参数效验
    if (aid == null || aid <= 0) {
        return ResultAjax.fail(-1, "参数有误!");
    }
    // 2. 得到当前登录用户 id
    Userinfo userinfo = SessionUtils.getUser(request);
    if (userinfo == null) {
        return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");
    }
    // 3. 查询文章并效验权限 where id=#{aid} and uid=#{uid}
    Articleinfo articleinfo = articleService.getArticleByIdAndUid(
            aid, userinfo.getId()
    );
    // 4. 将结果返回给前端
    return ResultAjax.succ(articleinfo);
}

♨️涉及到的 sql 接口

// 修改文章中间步骤: 查询自己发表的文章详情
@Select("select * from articleinfo where id=#{aid} and uid=#{uid}")
Articleinfo getArticleByIdAndUid(@Param("aid") int aid, @Param("uid") int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

<script>
	 var aid = getParamValue("aid"); // 文章id
	// 初始化页面的方法
    function init() {
        // 1. 效验 aid
        if (aid == null || aid <= 0) {
            alert("非法参数!");
            return false;
        }
        // 2. 查询文章详情
        jQuery.ajax({
            url: "/art/update_init",
            type: "GET",
            data: {
                "aid": aid
            },
            success: function (res) {
                // 3. 将文章的详情信息展示到页面
                if (res.code == 200 && res.data != null && res.data.id > 0) {
                    // 查询到了文章信息
                    jQuery("#title").val(res.data.title);
                    initEdit(res.data.content);
                } else if (res.code == -2) {
                    alert("出错了: 操作失败, 请重新操作! " + res.msg);
                    location.href = "login.html";
                } else {
                    alert("出错了: 操作失败, 请重新操作! " + res.msg);
                }
            }
        });
    }

    init();
}
13.2.6.2. 发布修改后的文章

♨️前后端交互接口
后端:

  1. /art/update
  2. 返回受影响行数

前端:

  1. POST,json,/art/update
  2. data:id(文章id),title,content

♨️后端实现

/**
 * 修改文章信息
 */
@RequestMapping("/update")
public ResultAjax update(Articleinfo articleinfo, HttpServletRequest request) {
    // 1. 参数效验
    if (articleinfo == null ||
            !StringUtils.hasLength(articleinfo.getTitle()) ||
            !StringUtils.hasLength(articleinfo.getContent()) ||
            articleinfo.getId() == 0) {
        return ResultAjax.fail(-1, "非法参数!");
    }
    // 2. 获取登录用户
    Userinfo userinfo = SessionUtils.getUser(request);
    if (userinfo == null) {
        return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");
    }
    articleinfo.setUid(userinfo.getId());
    // 3. 修改文章, 并效验归属人
    int result = articleService.update(articleinfo);
    // 4. 返回结果
    return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

 // 修改文章, 并效验归属人
@Update("update articleinfo set title=#{title},content=#{content} where id=#{id} and uid=#{uid}")
int update(Articleinfo articleinfo);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 执行修改操作
function doUpdate() {
    // 1. 效验参数
    var title = jQuery("#title");
    if (title.val().trim() == "") {
        alert("请先输入标题! ");
        title.focus();
        return false;
    }
    if (editor.getValue() == "") {
        alert("请先输入正文! ");
        return false;
    }
    // 2. 将结果提交给后端
    jQuery.ajax({
        url: "/art/update",
        type: "POST",
        data: {
            "id": aid,
            "title": title.val(),
            "content": editor.getValue()
        },
        success: function (res) {
            if (res.code == 200 && res.data == 1) {
                // 修改成功
                alert("文章修改成功! ");
                // 跳转到我的文章管理员
                location.href = "myblog_list.html";
            } else if (res.code == -2) {
                alert("当前未登录, 请在登录后操作! ");
                location.href = "login.html";
            } else {
                alert("出错了: 修改失败, 请重新操作! " + res.msg);
            }
        }
    });
    // 3. 将后端返回的结果展现给用户
}

13.2.7. 根据分页来查询汇总列表

♨️前后端交互接口

后端:

  1. /art/getlistbypage
  2. size(最大页码),list(一页的博客列表信息)

前端:

  1. GET,quary string,/art/getlistbypage
  2. data:pindex(页码)& psize(页内最大博客数)

♨️后端实现

后端只要知道页码和一页多少条数据,就可以计算出要选取哪几条数据然后返回。

psize:一页几条数据,就是limit后面的值

pindex:根据这个值可以计算储偏移量为psize × (pindex - 1),就是offset后面的值。

所有博客数 / psize,向上取整就是最大页码size,如果所有博客数为 0,则前端应该显示,当前在第 0 页,共 0 页。

pindex正常操作下是不会出错的,因为前端知道最大页码size,会做出判断。

 /**
     * 查询所有博客: 分页查询
     */
    @RequestMapping("/getlistbypage")
    public ResultAjax getListByPage(Integer pindex, Integer psize) throws ExecutionException, InterruptedException {
        // 1. 参数矫正
        if (pindex == null || pindex < 1) {
            pindex = 1; // 参数矫正
        }
        if (psize == null || psize < 1) {
            psize = 2; // 参数矫正
        }
        // 2. 并发进行文章列表和总页数的查询
        // 2.1 查询分页列表数据
        int finalOffset = psize * (pindex - 1); // 分页公式 (偏移位置, 即从第几条数据开始查)
        int finalPSize = psize;
        FutureTask<List<Articleinfo>> listTask = new FutureTask<>(() -> {
            List<Articleinfo> list = articleService.getListByPage(finalPSize, finalOffset);
            if (list != null && list.size() > 0) {
                // 并行处理 list 集合
                list.stream().parallel().forEach((art) -> {
                    if (art.getContent().length() > _DESC_LENGTH) {
                        // 截取
                        art.setContent(art.getContent().substring(0, _DESC_LENGTH));
                    }
                });
            }
            return list;
        });
        // 2.2 查找总页数
        FutureTask<Integer> sizeTask = new FutureTask<>(() -> {
            // 总条数
            int totalCount = articleService.getCount();
            double sizeTemp = (totalCount * 1.0) / finalPSize;
            // 向上取整
            return (int) Math.ceil(sizeTemp);
        });
        taskExecutor.submit(listTask);
        taskExecutor.submit(sizeTask);
        // 3. 组装数据
        List<Articleinfo> list = listTask.get();
        int size = sizeTask.get();
        HashMap<String, Object> map = new HashMap<>();
        map.put("list", list);
        map.put("size", size);
        // 4. 将结果返回给前端
        return ResultAjax.succ(map);
    }

♨️涉及到的 sql 接口

// 查询一页的文章列表
@Select("select * from articleinfo order by id desc limit #{psize} offset #{offset}")
public List<Articleinfo> getListByPage(@Param("psize") int psize, @Param("offset") int offset);
// 查询文章表记录数
@Select("select count(*) from articleinfo")
int getCount();

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/art/getlistbypage")

♨️前端逻辑及事件

img

<script>
    var psize = 2; // 每页显示条数
    var pindex = 1; // 页码
    var totalpage = 1; // 总共有多少页
    // 初始化数据
    function init() {
        // 1. 处理分页参数
        psize = getParamValue("psize");
        if (psize == null) {
            psize = 2; // 每页显示条数
        }
        pindex = getParamValue("pindex");
        if (pindex == null) {
            pindex = 1; // 页码
        }
        jQuery("#pindex").html(pindex);
        // 2. 请求后端接口
        jQuery.ajax({
            url: "/art/getlistbypage",
            type: "GET",
            data: {
                "pindex": pindex,
                "psize": psize
            },
            success: function (res) {
                // 3. 将结果展示给用户
                if (res.code == 200 && res.data != null) {
                    var createHtml = "";
                    if (res.data.list != null && res.data.list.length > 0) {
                        // 有文章
                        totalpage = res.data.size;
                        jQuery("#pszie").html(totalpage);
                        var artlist = res.data.list;
                        for (var i = 0; i < artlist.length; i++) {
                            var art = artlist[i]; // 文章对象
                            createHtml += '<div class="blog" >';
                            createHtml += '<div class="title">' + art.title + '</div>';
                            createHtml += '<div class="date">' + art.createtime + '</div>';
                            createHtml += '<div class="desc">' + art.content + '</div>';
                            createHtml += '<a href="blog_content.html?aid=' +
                                art.id + '" class="detail">查看全文</a>';
                            createHtml += '</div>';
                        }
                    } else {
                        // 暂无文章
                        createHtml += '<h3 style="margin-top:20px;margin-left:20px;">不好意思, 暂无文章! </h3>';
                    }
                    jQuery("#artListDiv").html(createHtml);
                } else {
                    alert("出错了: 查询失败,  请重新操作! " + res.msg);
                }
            }
        });
    }

    init();

    // 点击首页
    function doFirst() {
        // 1. 判断是否在首页
        if (pindex <= 1) {
            alert("当前已经是第一页了哦! ");
            return false;
        }
        // 2. 跳转到首页
        location.href = "blog_list.html";
    }

    // 点击末页
    function doLast() {
        // 1. 判断是否在末页
        if (pindex >= totalpage) {
            alert("当前已经是最后一页了哦! ");
            return false;
        }
        // 2. 跳转到末页
        location.href = "blog_list.html?pindex=" + totalpage;
    }

    // 点击 "上一页"
    function doBefore() {
        // 1. 判断是否在首页
        if (pindex <= 1) {
            alert("当前已经是第一页了哦! ");
            return false;
        }
        // 2. 跳转上一页
        location.href = "blog_list.html?pindex=" + (parseInt(pindex) - 1);
    }

    // 点击 "下一页"
    function doNext() {
        // 1. 判断是否在末页
        if (pindex >= totalpage) {
            alert("当前已经是最后一页了哦! ");
            return false;
        }
        // 2. 跳转到下一页
        location.href = "blog_list.html?pindex=" + (parseInt(pindex) + 1);
    }
</script>

14. Session升级存储到Redis

添加依赖:

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

添加redis配置信息(properties):

# redis 配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=2
spring.session.store-type=redis

15. 项目部署

1️⃣Linux 中安装 Redis

使用以下命令,直接将 redis 安装到 linux 服务器:

yum -y install redis

2️⃣设置 Redis 远程连接

这一步需要修改 Redis 中的一些配置

  1. 进入 redis 配置文件的编写模式,redis 配置文件就是 linux 下的 /etc/redis.conf。
  2. 将 redis.conf 中的 “bind 127.0.0.1” 注释掉。
  3. 将 redis.conf 中的 “protected-mode yes” 改为 “protected-mode no”。
  4. 保存并退出。
  5. 使用命令 “redis-cli shutdown” 先关闭 redis 服务,再使用 “redis-server /etc/redis.conf &” 启动 redis 服务。
  6. redis 在服务器的端口默认是 6379,配置防火墙或者安全组将这个端口开放。

3️⃣启动 Redis

使用以下命令,以后台运行方式启动 redis:

redis-server /etc/redis.conf &

4️⃣打包上传项目

将程序打包为.jar包上传到云服务器。

要注意,在打包项目的时候,⼀定要检查,确保数据库连接的是远程服务器的 MySQL,确保密码正确;确保 Rdeis 端口配置正确。

5️⃣启动项目

使⽤以下命令启动 Spring Boot 项⽬并后台运行:

nohup java -jar xxx.jar &

6️⃣停止项目

停止 Spring Boot 项目需要两步:

  1. 查询出运行的 Spring Boot 的进程,使用命令:
ps -ef | grep java
  1. 将 Spring Boot 的进程结束掉,使用命令:
kill -9 进程ID

16. 项目亮点

  1. 应用到了多线程提高业务处理效率。
  2. 列表显示实现了一个分页功能。
  3. 密码的存储使用了自己写加盐加密算法。
  4. 用到了AOP编程,统一处理与拦截器。
  5. Session 存储到 Redis,可以让多个服务器共享 Session 数据。