SSM项目-博客系统

在线体验项目:登陆页面

项目连接:huhublog_ssm: 个人博客系统 技术栈:SpringBoot、SpringMVC、Mybatis、Redis、JQuery、Ajax、Json (gitee.com)

1.项目技术点分析 

SpringBoot、SpringWeb(SpringMVC)、MyBatis、MySQL(8.x)、Redis(存储验证码及用户登录信息)

2.数据库设计分析

注册页面、登录页面 --> 用户表(显式字段:用户编号、登录用户名、密码) 不需要(确认密码只是前端使用一下,验证码存到redis)(隐式字段:用户状态(正常,异常,被永久冻结,被临时冻结))

博客列表页面 --> 用户表(显示字段:头像、昵称、个人gitee地址)

博客列表、添加、修改页面、文章详情页面 --> 文章表(显式字段:文章编号、标题、创建时间、修改时间、文章详情、文章正文)(隐式字段:文章状态(发布状态、草稿)、访问量、用户主键(文章作者id))

文章详情页面 --> 评论表(显式字段:评论表主键、文章编号、用户主键(评论发表人是谁)、评论的正文、评论的发表时间)

3.准备工作

3.1准备数据库

-- 创建数据库
drop database if exists huhublog;
create database huhublog DEFAULT CHARACTER SET utf8mb4;
 
-- 使用数据库
use huhublog;

-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
	uid bigint auto_increment primary key comment '用户编号',
    username varchar(100) not null unique comment '用户名', 
	nickname varchar(100) default '' comment '昵称',
    password varchar(65) not null comment '密码',
	gitee varchar(255) comment 'gitee地址',
    photo varchar(255) comment '头像',
    state tinyint not null default 1 comment '用户状态,1=正常|2=异常|3=永久冻结|4=临时冻结'
) default charset 'utf8mb4';

-- 创建表[文章表]
drop table if exists articleinfo;
create table articleinfo(
	aid bigint auto_increment primary key comment '文章编号',
	title varchar(100) not null comment '文章标题',
	createtime timestamp not null default CURRENT_TIMESTAMP comment '创建时间',
	updatetime timestamp not null default CURRENT_TIMESTAMP comment '修改时间',
	description varchar(255) not null comment '文章简介',
	content longtext not null comment '文章正文',
	state tinyint default 1 comment '状态:1=草稿|2=发布',
	uid bigint not null comment '作者id',
	rcount bigint default 1 comment '阅读量'
)default charset 'utf8mb4';

-- 创建表[评论表]
drop table if exists commentinfo;
create table commentinfo(
	cid bigint auto_increment primary key comment '评论编号',
	aid bigint not null comment '文章编号',
	uid bigint not null comment '用户编号',
	content varchar(255) not null comment '评论正文',
	createtime timestamp default CURRENT_TIMESTAMP() comment '评论发表时间'
)default charset 'utf8mb4'; 
 

总共三张表用户表、文章表、评论表

SSM项目-博客系统_第1张图片

SSM项目-博客系统_第2张图片SSM项目-博客系统_第3张图片

SSM项目-博客系统_第4张图片

3.2搭建开发环境创建项目

SSM项目-博客系统_第5张图片

3.3准备项目的配置文件

在resources 文件下创建一个 application.properties 的文件, 并根据自己的数据库信息和mapper 文件夹的命名将以下 xml 文件配置到 application.properties 文件中.

# 配置连接的 mysql 数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/huhublog?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=2337144
# 设置 Mybatis 的 xml 保存路径
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
# 配置 Mybatis 执行的SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging.level.com.huhu=debug

准备好properties配置文件后, 根据里面的mapper-locations中的约定创建一个叫做 mapper 的文件夹, 并在文件夹下创建三个以 Mapper.xml 为后缀名的文件.

SSM项目-博客系统_第6张图片

在.xml文件添加格式代码拿UserMapper举例





3.4准备项目的公共模块

1控制层(controller) - 控制器

2业务层(service) - 业务类

3持久层(mapper) - mapper

4实体层(entity) - 实体类

5工具层(util) - 统一返回结果类

3.4.1控制层(拿User举例)

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

3.4.2业务层(这里只显示接口实现类,具体代码看网址)

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;
}

3.4.3持久层 

@Mapper
public interface UserMapper {
}

3.4.4实体层

@Data
public class UserInfo {
    private long uid;
    private String username;
    private String nickname;
    private String password;
    private String gitee;
    private String photo;
    private int state;
}
@Data
public class ArticleInfo {
    private long aid;
    private String title;
    private String description;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private String content;
    private int state;      //文章状态
    private long uid;       //作者id
    private long rcount;    //l阅读人数
}
@Data
public class CommentInfo {
    private long cid;
    private long aid;
    private long uid;
    private String content;
    private LocalDateTime createtime;
}

3.4.5工具层(统一返回类)

1.使用自定义拦截器LoginInterceptor

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
            return true;
        }
        // 401 : 用户没有登录所有没有权限  403 : 用户登录了但没有权限
        response.setStatus(401);
        return false;
    }
}

2.配置拦截规则AppConfig

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
 
    List excludes = new ArrayList() {{
        add("/**/*.html");
        add("/js/**");
        add("/editor.md/**");
        add("/css/**");
        // add("/**/*.jsp"); // 这样写穷举不完
        add("/img/**"); // 放行 img 下的所有文件
        add("/user/login"); // 放行登录
        add("/user/reg"); // 放行注册
        add("/art/detail"); // 放行详情页
        add("/art/rcount");// 放行访问人数
        add("/getcaptcha");// 放行验证码
        add("/image/**");// 放行本地图片验证码
        add("/art/list"); // 放行文章分页列表的接口
        add("/art/totalpage"); // 放行获取文章分页的总页数
        add("/comment/list");   // 放行评论列表
        add("/user/myinfo");
        add("/user/info");
        add("/user/isartbyme");

    }};

 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration =
                registry.addInterceptor(loginInterceptor);
        registration.addPathPatterns("/**");
        registration.excludePathPatterns(excludes);
    }
}

3.统一异常的处理

@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class) // 异常类型
    public Object exceptionAdvice(Exception e) {
        return AjaxResult.fail(-1, e.getMessage());
    }
}

4.统一数据的返回

对返回的数据进行分类:成功返回/失败

定义一个数据返回类 - AjaxResult.java

/**
 * 接口统一返回对象
 */
public class AjaxResult {
    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap success(Object data) {
        HashMap result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", data);
        return result;
    }

    /**
     * 业务执行成功时进行返回的方法
     * @param data
     * @return
     */
    public static HashMap success(String msg, Object data) {
        HashMap result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }

    /**
     * 业务执行失败时进行返回的方法
     * @param code
     * @param msg
     * @return
     */
    public static HashMap fail(int code, String msg) {
        HashMap result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", "");
        return result;
    }

    /**
     * 业务执行失败时进行返回的方法
     * @param code
     * @param msg
     * @param data
     * @return
     */
    public static HashMap fail(int code, String msg, Object data) {
        HashMap result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }
}

定义一个将返回数据类进行统一处理的类 - ResponseBodyAdvice.java 

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

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

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 1.本身已经是封装好的对象
        if(body instanceof HashMap) {
            return body;
        }
        // 2.返回类型是 String (特殊)
        if(body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);
    }
}

5.统一session验证

public class SessionUtil {
    public static UserInfo getLoginUser(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
            return (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
        }
        return null;
    }
}

6.统一常量类

此处的常量类主要是用户登录成功后, 将其身份信息存储 session 时, 需要多次写字符串常量, 为了避免写错, 所以专门定义一个常量类, 就只需要写一次即可.

public class Constant {
    public static final String SESSION_USERINFO_KEY = "SESSION_USERINFO_KEY";
}

4.实现相关功能

4.1实现注册功能

前端操作reg.html页面

第一步: 引入用于发送 ajax 请求的依赖

第二步 :约定前后端交互接口

url :"/user/reg"
type : "POST"
data : "username,password"
后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.

第三步:写前端代码

1.分别给用户名、密码、确认密码、验证码加上id属性,并给提交按钮绑定点击事件


        

2.点击提交按钮触发ajax请求

function doReg() {
            // 进行非空校验
            var username=jQuery('#username')
            var password=jQuery('#password')
            var password2=jQuery('#password2')
            var checkCode=jQuery('#code_input')
            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(checkCode.val().trim()==''){
                alert('请输入验证码!')
                checkCode.focus()
                return false
            }

            // 密码与确认密码是否一致
            if(password.val()!=password2.val()){
                alert('两次密码输入不一致,请重新输入密码!')
                password.focus()
                return false
            }
            // 发送Ajax请求
            jQuery.ajax({
                url:'/user/reg',
                type:'post',
                data:{
                    "username":username.val(),
                    "password":password.val(),
                    "checkCode":checkCode.val()
                },
                // 接受返回结果
                success:function(body) {
                    if(body.code==200 && body.data==1) {
                        alert("恭喜,注册成功!");
                        if(confirm("是否要去登录页面 ?")) {
                            location.href = "login.html";
                        }
                    } else if(body.data == -1) {
                        alert("抱歉, 注册失败, 请重新注册!")
                    } else {
                        alert("该用户名已被使用, 请重新输入!");
                    }
                }
            }) 
        }

第四步写后端代码

注册功能即数据库新增用户功能

步骤一:先从mapper层写起(UserMapper)

// 新增用户(注册功能)
    public int addUser(UserInfo userInfo);

步骤二:写配置文件.xml(UserMapper.xml)


        insert into userinfo(username,password) values (#{username},#{password})

步骤三:写service层代码(UserServiceImpl)

 @Override
    public int addUser(UserInfo userInfo) {
        return userMapper.addUser(userInfo);
    }

步骤四:写controller层代码(UserController)

/**
     * 注册功能实现
     * @param userInfoVO
     * @return
     */
    @RequestMapping("/reg")
    public Object reg(UserInfoVO userInfoVO) {
        // 1.非空校验
        if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())
                || !StringUtils.hasLength(userInfoVO.getPassword())
                || !StringUtils.hasLength(userInfoVO.getCheckCode())) {
            return AjaxResult.fail(-1, "非法参数请求!");
        }
        // 2.进行数据库添加操作
        int result = userService.addUser(userInfoVO);
        if (result == 1) {
            return AjaxResult.success("注册成功!", 1);
        } else {
            return AjaxResult.fail(-1, "数据库添加出错!");
        }
    }

此处先进行明文存储, 方便后续操作; 等全部功能基本实现时, 再将密码升级为加盐存储.

另外注册页面导航栏的右上角只有登录和主页两个按钮:

登录 : 有些用户已经有账号了, 就可以点击跳转到登录页面;

主页 : 没有登录也是可以访问主页别人写的博客的(例如CSDN、博客园等)

页面效果:

4.2实现登录功能

前端操作login.html页面

第一步: 引入用于发送 ajax 请求的依赖

第二步 :约定前后端交互接口

url :"/user/login"
type : "POST"
data : "username,password"

后端返回 data=1, code=200 表示代码执行成功, 返回 data != 1, code != 200 表示执行失败.

第三步:写前端代码

1.分别给用户名、密码、确认密码、验证码加上id属性,并给提交按钮绑定点击事件


        

 2.点击提交按钮触发ajax请求

function doLogin() {
            // 非空参数的校验
            var username=jQuery('#username')
            var password=jQuery('#password')
            var checkCode=jQuery('#code_input')

            if(username.val().trim()==''){
                alert('请输入用户名!')
                username.focus()
                return false
            }
            if(password.val().trim()==''){
                alert('请输入密码!')
                password.focus()
                return false
            }
            if(checkCode.val().trim()==''){
                alert('请输入验证码!')
                checkCode.focus()
                return false
            }

            // 2. 发送请求给客户端
    jQuery.ajax({
        url:"/user/login",
        type:"post",
        data:{
            // 前面的 username 可以加引号, 也可以不加
            username:username.val(),
            password:password.val(),
            checkCode:checkCode.val()
        },
        success:function(body) {
            if(body.code == 200 && body.data == 1) {
                // alert("登录成功!");
                location.href = "myblog_list.html";
            } else {
                alert("用户名或密码错误, 请重新输入!");
                username.focus();
            }
        }
    });
}

第四步:写后端代码

登录功能即查找用户是否存在

步骤一:先mapper层写起 

// 查询用户
    public UserInfo getUserByUsername(@Param("username") String username);

步骤二:写映射配置文件.xml

步骤三:写service层代码

@Override
    public UserInfo getUserByUsername(String username) {
        return userMapper.getUserByUsername(username);
    }

步骤四:写controller层代码

/**
     * 登录功能实现
     * @param userInfoVO
     * @param request
     * @return
     */
    @RequestMapping("/login")
    public int login(UserInfoVO userInfoVO, HttpServletRequest request) {
        // 1.非空校验
        if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())
                || !StringUtils.hasLength(userInfoVO.getPassword())
                || !StringUtils.hasLength(userInfoVO.getCheckCode())) {
            return 0;
        }
        // 2.查询操作
        // 2.1.通过用户名查找对象
        UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());
        // 2.2 如果对象为空或者为无效对象,则返回-1
        if (userInfo == null || userInfo.getUid() <= 0) {
            return -1;
        }
        // 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1
        if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {
            return -1;
        } else {
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
            return 1;
        }
    }

此处的登录功能也是没有通过明文查询, 后面升级为加盐处理时, 会有对应的解密方法来处理, 后面再进行代码的大改.

另外登录页面导航栏的右上角只有注册和主页两个按钮:

注册 : 有些用户需要注册, 但是不小心点到登录界面, 所以对于用户来说, 两个页面需要来回跳转.

主页 : 像CSDN网站没有登录也是可以访问主页别人写的博客的.

页面效果: 

此处就可以在工具包下添加:常量类Constant存储用户信息、拦截器LoginInterceptor类和AppConfig配置类

4.3实现退出登录功能

前端操作myblog_list.html页面

第一步: 引入用于发送 ajax 请求的依赖

第二步 :约定前后端交互接口

url :"/user/logout"
type : "POST"
data : {}        //不传,即空对象
后端如果返回状态码200,就触发success分支,如果返回状态码401,触发error分支,两分支一样,都是跳转到登录页面

第三步:写前端代码

function onExit() {
        if(confirm("确认退出? ")) {
            jQuery.ajax({
                url:"/user/logout",
                type:"POST",
                data:{},
                // 200
                success:function(body) {
                    // alert(JSON.stringify(body)); // 将 json 转换成 String
                    location.href = "/login.html";
                },
                // 非 200
                error:function(err) {
                    // alert(JSON.stringify(err))
                    if(err != null && err.status==401) {
                        alert("用户未登录, 即将跳转到登录页!")
                        // 已经被拦截器拦截了 - 未登录
                        location.href = "/login.html";
                    }
                }
            });
        }
    }

第四步:写后端代码

// 退出登录 【拦截】
    @RequestMapping("/logout")
    public boolean logout(HttpServletRequest request) {
        // 设置false原因是:如果request里面没有session,那么不会去新建session
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
            // 清除 session
            session.removeAttribute(Constant.SESSION_USERINFO_KEY);
        }
        // 默认返回 true, 如果不清除 session, session 也有一个过期时间 30分钟
        return true;
    }

注意:登录情况下点击退出登录结果如图

SSM项目-博客系统_第7张图片

未登录情况下点击退出登录结果如图

SSM项目-博客系统_第8张图片

4.4实现登录人的博客列表页

前端操作myblog_list.html

该页面要实现的功能:
1. 展现登录人写的所有博客, 如果此人没写博客, 就显示暂无数据.
2. 在左侧展示登录人的个人信息, 此处要动态展示的个人信息有用户名(username)和以及文章数量

功能一:展示所有博客 

第一步: 引入用于发送 ajax 请求的依赖

第二步:约定前后端接口 

url : "art/mylist"
type : "GET"
data : 不传, 让后端从 session 中拿, 避免出现张三传了李四的 id, 把李四的文章查出来了.
后端返回 code == 200, 如果 data 不为空, 且 data.length > 0 表示该作者写了文章, 否则表示该作者没有文章. 后端返回非 200 就走 error 分支.

第三步:写前端代码

 // 获取右侧文章列表
    function initMyList() {
        jQuery.ajax({
            url: '/art/mylist',
            type: 'GET',
            data: {},
            success: function (body) {
                // 获取右侧的容器
                var rightContainer = document.querySelector('.container-right');
                if (body.code == 200 && body.data != null && body.data.length > 0) {
                    // 遍历body的博客列表
                    for (let blog of body.data) {
                        let blogDiv = document.createElement('div');
                        blogDiv.className = 'blog';
                        // 创建博客标题
                        let titleDiv = document.createElement('div');
                        titleDiv.className = 'title';
                        titleDiv.innerHTML = blog.title;
                        blogDiv.appendChild(titleDiv);
                        // 创建发布日期
                        let dateDiv = document.createElement('div');
                        dateDiv.className = 'date';
                        dateDiv.innerHTML = blog.createtime;
                        blogDiv.appendChild(dateDiv);
                        // 创建博客简介
                        let descDiv = document.createElement('div');
                        descDiv.className = 'desc';
                        // descDiv.id='descMark';
                        // 博客简介即截取文章内容前100个字符
                        var content = subContent(blog.content);
                        descDiv.innerHTML = content;
                        blogDiv.appendChild(descDiv);
                        // 创建一个div
                        let div = document.createElement('div');
                        div.className = 'ownDetail';
                        // 创建查看详情按钮
                        let a1 = document.createElement('a');
                        a1.innerHTML = '查看详情>>';
                        a1.style = 'border:2px solid black;background-color:#eaea9a';
                        a1.href = 'blog_content.html?aid=' + blog.aid;
                        div.appendChild(a1);
                        // 创建修改按钮
                        let a2 = document.createElement('a');
                        a2.innerHTML = '修改>>';
                        a2.style = 'border:2px solid black;background-color:#d9ecf2';
                        a2.href = 'blog_edit.html?aid=' + blog.aid;
                        div.appendChild(a2);
                        // 创建删除按钮
                        let a3 = document.createElement('a');
                        a3.innerHTML = '删除>>';
                        a3.style = 'border:2px solid black;background-color:#ed9898';
                        a3.href = 'javascript:del(' + blog.id + ')';
                        div.appendChild(a3);
                        // 把这三个按钮添加到外层元素
                        blogDiv.appendChild(div);
                        // 将博客盒子加入右侧容器中
                        rightContainer.appendChild(blogDiv);
                    }
                } else {
                    // 未发表任何数据
                    let blogDiv = document.createElement('div');
                    blogDiv.innerHTML = "

暂无数据

"; blogDiv.style = "text-align: center"; rightContainer.appendChild(blogDiv); } }, error: function (err) { if (err != null && err.status == 401) { // alert("用户未登录, 即将跳转到登录页!") // 已经被拦截器拦截了 - 未登录 location.href = "/login.html"; } } }) }; initMyList();

此处的文章列表页展示的内容只是博客的一部分, 这里规定展示前 100 个字符为博客简介操作, 并且该接口在主页中所有人的列表页也需要调用, 所以将前面的内容截取函数封装在一个公共的 js 方法中. 

js/util.js

var descLength=100;
function subContent(content) {
    if (content.length>descLength){
        return content.substring(0,descLength);
    }
    return content;
}

第四步:写后端代码

步骤一:先从mapper层写起

// 获取我的所有文章
    public List getMyList(@Param("uid") Long uid);

步骤二:映射配置文件.xml

步骤三:业务层

@Override
    public List getMyList(Long uid) {
        return articleMapper.getMyList(uid);
    }

步骤四:控制层

/**
     * 获取我的所有文章
     * @param request
     * @return
     */
    @RequestMapping("/mylist")
    public List getMyList(HttpServletRequest request){
        // 在session中验证登录状态
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        // 如果获取用户对象为空或者无效对象
        if (userInfo==null||userInfo.getUid()<=0){
            return null;
        }
        return articleService.getMyList(userInfo.getUid());
    }

我的文章列表页要实现的功能还有很多, 先看看整体页面:

我的博客列表页实现的功能有 : 查看详情, 修改 , 删除, 以及左侧个人信息的 username, 以及文章数量. 后面会一一实现. 

功能二:获取博客列表页的个人信息

第一步:约定前后端接口

url : "/user/myinfo"
type : "GET"
data : {} //不传
当后端返回 200 , 并且 data 不为空, 就构造数据, 否则就是未登录, 直接跳转到登陆页面.

第二步:写前端代码

// 获取个人信息
    function getMyInfo() {
        jQuery.ajax({
            url:'/user/myinfo',
            type:'GET',
            data:{},
            success:function (body) {
                let h3=document.querySelector('.card h3');
                h3.innerHTML=body.data.username;
            },
            error:function (err) {
                location.href='/login.html'
            }
        })
    }

    getMyInfo();

第三步:写后端代码

/**
     * 获取个人信息
     * @param request
     * @return
     */
    @RequestMapping("/myinfo")
    public UserInfo myinfo(HttpServletRequest request){
        return SessionUtil.getLoginUser(request);
    }

功能三:获取登录人的文章数量

第一步:约定前后端接口

url : "/art/myartcount"
type : "GET"
data : 不传
后端返回 200 状态码并且 data 不为空就构造数据, 否则就是未登录.

第二步:写前端代码

// 获取文章数量
    function getMyArtCount() {
        jQuery.ajax({
            url:'/art/myartcount',
            type:'GET',
            data:{},
            success:function (body) {
                if (body.code=200&&body.data!=null){
                    let artSpan=document.querySelector('#cnt');
                    artSpan.innerHTML=body.data;
                }
            },
            error:function (err) {
                location.href='/login.html';
            }
        })
    }

    getMyArtCount();

第三步:写后端代码

步骤一:写mapper层

// 获取我的文章数量
    public int myArtCount(@Param("uid") Long uid);

步骤二:映射配置文件.xml

步骤三:service层

@Override
    public int myArtCount(Long uid) {
        return articleMapper.myArtCount(uid);
    }

步骤四:controller层

/**
     * 获取我的文章数量
     * @param request
     * @return
     */
    @RequestMapping("myartcount")
    public int myArtCount(HttpServletRequest request){
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo!=null&&userInfo.getUid()>0){
            return articleService.myArtCount(userInfo.getUid());
        }
        return 0;
    }

4.5实现公开文章列表分页功能

实现该功能之前, 先要知道一个公式, 下面推算一下这个公式 >>

1. 页码 (pIndex) : 要查询第几页的数据.
2. 每页显示的最大长度的数据 (pSize) : 每页显示多少条数据.

找规律:假设我们设置每页显示2条数据

第一页显示aid=1和aid=2的数据

SSM项目-博客系统_第9张图片

第二页显示aid=3和aid=4的数据

SSM项目-博客系统_第10张图片

由此得出一个公式偏移量offset=(pindex-1)*psize,例如第一页偏移量0=(1-1)*2,第二页偏移量2=(2-1)*2,最终分页语法查询后面接上limit psize offset  (pindex-1)*psize

公开文章列表顾名思义就是所有人都可以访问,因此不需要拦截,效果如下

SSM项目-博客系统_第11张图片

我们先处理页面上分页的数据,后面再实现四个按钮功能,先初始化pindex和psize(这两参数可可以从url传递,也可以不传)

// 分页功能的默认参数值
    var psize = 2; // 每页显示的文章数量
    var pindex = 1; // 当前页码

    // 初始化分页功能的的参数,从 url 中获取 pindex 和 psize
    function initPageParam() {
        var pageindex = getUrlParam("pindex");
        if (pageindex != "") {
            pindex = pageindex;
        }
        var pagesize = getUrlParam("psize");
        if (pagesize != "") {
            psize = pagesize;
        }
    }

    initPageParam();

页面数据功能实现

第一步:约定前后端接口

url : "/art/list"
type : "GET"
data : psize, pindex
后端返回 code == 200 并且 data != null 就构造数据. 

第二步:写前端代码

// 查询分页数据
    function initList() {
        jQuery.ajax({
            url: "/art/list",
            type: "GET",
            data: {
                "psize": psize,
                "pindex": pindex
            },
            success: function (body) {
                let listDiv = document.querySelector("#listDiv");
                if (body.code == 200 && body.data != null) {
                    // 遍历 body 中的 data
                    for (let blog of body.data) {
                        let blogDiv = document.createElement('div');
                        blogDiv.className = 'blog';
                        // 创建博客标题
                        let titleDiv = document.createElement('div');
                        titleDiv.className = 'title';
                        titleDiv.innerHTML = blog.title;
                        blogDiv.appendChild(titleDiv);
                        // 创建发布日期
                        let dateDiv = document.createElement('div');
                        dateDiv.className = 'date';
                        dateDiv.innerHTML = formatDate(blog.createtime);
                        blogDiv.appendChild(dateDiv);
                        // 创建摘要
                        let descDiv = document.createElement('div');
                        descDiv.className = 'desc';
                        descDiv.innerHTML = subContent(blog.content);
                        blogDiv.appendChild(descDiv);

                        // 创建查看全文按钮
                        let a = document.createElement('a');
                        a.className = 'listdetail';
                        a.innerHTML = '查看详情 >>';
                        a.href = 'blog_content.html?id=' + blog.id;
                        blogDiv.appendChild(a);

                        // 把 blogDiv 加入外层元素
                        listDiv.appendChild(blogDiv);
                    }
                }
            }
        });
    }

    initList();

第三步:写后端代码

步骤一:写mapper层

// 获取分页列表页
    public List getList(@Param("psize") Integer psize,
                                     @Param("offset") Integer offset);

步骤二:映射配置文件.xml

步骤三:写service层

@Override
    public List getList(Integer psize, Integer offset) {
        return articleMapper.getList(psize,offset);
    }

步骤四:写controller层

/**
     * 获取分页列表
     * @param psize
     * @param pindex
     * @return
     */
    @RequestMapping("/list")
    public List getList(Integer psize, Integer pindex) {
        if (psize == null || pindex == null) {
            return null;
        }
        // 分页公式 : 计算偏移量
        int offset = psize * (pindex - 1);
        return articleService.getList(psize, offset);
    }

接下来实现四个按钮功能,给按钮绑定点击事件




首页按钮、上一页按钮功能实现 

// 首页
    function firstPage() {
        location.href = "blog_list.html";
    }

    // 上一页
    function prevPage() {
        if (pindex <= 1) {
            alert("当前已经是第一页了!");
            return false;
        }
        pindex = parseInt(pindex) - 1;
        location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;
    }

末页按钮、下一页按钮功能实现

实现之前需要先统计文章数量,然后计算出分页列表总共有多少页数据,并且该接口也是不能拦截的(因为这也是所有人都可以访问的页面所需的接口)

总页数(向上取整):totalPage = Math.ceil (total * 1.0 / psize)  

第一步:约定前后端接口

url : "/art/totalpage"
type : "GET"
data : psize
后端返回 code == 200 并且 data 不为空, 就构造数据.

第二步:写前端代码

// 查询总共有多少页的数据
    function getTotalPage() {
        jQuery.ajax({
            url: "/art/totalpage",
            type: "get",
            data: {
                "psize": psize
            },
            success: function (body) {
                if (body.code == 200 && body.data != null) {
                    totalpage = body.data;
                }
            }
        });
    }

第三步:写后端代码

步骤一:写mapper层

// 查询数据库总共有多少条数据(所有文章)
    public int getTotalCount();

步骤二:映射配置文件.xml

步骤三:写service层

@Override
    public int getTotalCount() {
        return articleMapper.getTotalCount();
    }

步骤四:写controller层 

/**
     * 计算分页功能的总页数 【不拦截】
     * @param psize
     * @return
     */
    @RequestMapping("/totalpage")
    // 此处返回包装类, 是为了和前端中的 body.data != null相对应
    public Integer totalPage(Integer psize) {
        if (psize != null) {
            // 数据库中总条数
            int totalCount = articleService.getTotalCount();
            // 总页数
            int totalPage = (int) Math.ceil(totalCount * 1.0 / psize);
            return totalPage;
        }
        return null;
    }

实现下一页和末页的代码

// 下一页
    function nextPage() {
        if (parseInt(pindex) + 1 > totalpage) {
            alert("当前已经是最后一页了!");
            return false;
        }
        location.href = "blog_list.html?pindex=" + (parseInt(pindex) + 1) + "&psize=" + psize;
    }

    // 末页
    function lastPage() {
        pindex = totalpage;
        location.href = "blog_list.html?pindex=" + pindex + "&psize=" + psize;
    }

4.6实现添加文章功能

添加文章需要登录了才能进行添加文章的操作, 所以需要过拦截器.(前端操作 blog_add.html)

第一步:约定前后端接口

url : "/art/add"
type : "POST"
data : title, content
后端返回 code == 200 并且返回data ==1 , 就表示发布成功. 如果 401, 则表示未登录.

第二步:写前端代码

// 发布文章
        function mysub() {
            // alert(editor.getValue()); // 获取值
            // editor.setValue("#123") // 设置值

            // 1.非空校验
            var title=jQuery('#title');
            if (title.val().trim()==""){
                alert('请先输入标题');
                title.focus();
                return false;
            }
            if(editor.getValue().trim()==""){
                alert('请先输入正文');
                return false;
            }
            // 2.将前端输入的内容发送给后端
            jQuery.ajax({
                url:'/art/add',
                type:'POST',
                data:{
                    "title":title.val(),
                    "content":editor.getValue()
                },
                success:function (body) {
                    // 3.将结果展示给用户
                    if (body.code=200&&body.data==1){
                        alert('文章添加成功!');
                        location.href='myblog_list.html';
                    }else{
                        alert('文章添加失败,请重试!');
                    }
                },
                error:function (err) {
                    if (err!=null&&err.status==401){
                        // 用户未登录,即将跳转到登录页
                        // 已经被拦截器拦截了,未登录
                        location.href="/login.html";
                    }
                }
            })
        }

第三步:写后端代码

步骤一:写mapper层

// 添加文章
    int addArticle(ArticleInfo articleInfo);

步骤二:映射配置文件.xml


        insert into articleinfo(uid,title,description,content) values (#{uid},#{title},#{description},#{content})
    

步骤三:写service层

@Override
    public int addArticle(ArticleInfo articleInfo) {
        return articleMapper.addArticle(articleInfo);
    }

步骤四:写controller层

/**
     * 添加文章
     * @param articleInfo
     * @param request
     * @return
     */
    @RequestMapping("/add")
    public Object addArtcile(ArticleInfo articleInfo, HttpServletRequest request) {
        // 1.非空校验(标题,内容)
        if (articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())
                || !StringUtils.hasLength(articleInfo.getContent())) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        // 2.组装数据(文章作者,标题,内容,简介)
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            // 无效对象或者对象为空
            return AjaxResult.fail(-2, "请先登录!");
        }
        // 作者
        articleInfo.setUid(userInfo.getUid());
        String description = articleInfo.getContent();
        if (description.length()>MAX_DESCRIPTION_LENGTH){
            description=description.substring(0,MAX_DESCRIPTION_LENGTH);
        }
        // 简介
        articleInfo.setDescription(description);
        int result = articleService.addArticle(articleInfo);
        if (result==1){
            return AjaxResult.success("添加成功!",1);
        }else{
            return AjaxResult.fail(-1,"添加失败!");
        }
    }

4.7实现修改文章功能

修改博客按钮在我的博客列表页才有, 主页的列表页没有修改功能. 并且修改功能是需要用户登录才能进行操作的, 所以需要过拦截器,前端操作myblog_list.html

实现修改博客有两个大的步骤 :
1. 从数据库中查询出文章的内容和标题, 然后初始化在 Markdown 编辑器上.
2. 修改文章内容/标题, 然后进行提交.

功能一:查询出文章内容和标题

第一步:约定前后端接口

url : "/art/mydetail"
type : "GET"
data : aid
后端返回状态码 200 并且 data 不为空, 就构造数据, 如果是 401 就跳转至登录页面.

第二步:写前端代码

// 获取文章详情
        function getArtDetail(){
            var aid=getUrlParam("aid");
            // 1.非空校验
            if (aid==null||aid==""){
                alert('参数有误!')
                return false;
            }
            // 2.发送ajax请求
            jQuery.ajax({
                url:'/art/mydetail',
                type:'GET',
                data:{
                    aid:aid
                },
                success:function (body) {
                    if(body.code == 200 && body.data != null) {
                        // 填充标题
                        jQuery("#title").val(body.data.title);
                        initEdit(body.data.content); // 初始化编译器的值 (待修改文章的正文)
                    } else {
                        // 强行访问别人的文章
                        alert('查询失败, 请重试!');
                    }
                },
                error:function (err) {
                    if (err!=null&&err.status==401){
                        // 用户未登录,即将跳转到登录页
                        // 已经被拦截器拦截了,未登录
                        location.href="/login.html";
                    }
                }
            })
        }

        getArtDetail();

3.写后端代码

步骤一:写mapper层

// 根据aid获取博客详情
public ArticleInfo getDetailByAid(Long aid);

步骤二:映射配置文件.xml

步骤三:写service层

@Override
    public ArticleInfo getDetailByAid(Long aid) {
        return articleMapper.getDetailByAid(aid);
    }

步骤四:写controller层 

 /**
     * 根据aid查询博客详情
     * @param aid
     * @return
     */
    @RequestMapping("/mydetail")
    public Object getMyDetail(Long aid){
        // 1.非空校验
        if (aid==null||aid<=0){
            return AjaxResult.fail(-1,"非法参数!");
        }
        // 2.操作数据库
        ArticleInfo articleInfo = articleService.getDetailByAid(aid);
        // 3.将结果返回给前端
        return AjaxResult.success(articleInfo);
    }

功能二:修改文章并提交

第一步:约定前后端接口

url : "/art/update"
type : "POST"
data : aid, title, content
后端返回 code == 200 并且返回data==1 就成功执行, 如果 401 就跳转至登录页面.

第二步:写前端代码

// 修改文章
        function mysub() {
            // alert(editor.getValue()); // 获取值
            // editor.setValue("#123") // 设置值

            // 1.参数校验
            var title=jQuery('#title');
            if (title.val().trim()==""){
                alert('请先输入标题');
                title.focus();
                return false;
            }
            if(editor.getValue().trim()==""){
                alert('请先输入正文');
                return false;
            }
            // 2.将数据发送给后端(文章标题、文章正文、文章id)
            jQuery.ajax({
                url:'/art/update',
                type:'POST',
                data:{
                    "title":title.val(),
                    "content":editor.getValue(),
                    "aid":aid   // 全局变量
                },
                success:function (body) {
                    // 3.将结果展示给用户
                    if (body.code==200&&body.data==1){
                        // 文章修改成功
                        alert('文章修改成功!');
                        location.href='/myblog_list.html';
                    }else{
                        alert('文章修改失败!');
                    }
                },
                error:function (err) {
                    if (err!=null&&err.status==401){
                        // 用户未登录,即将跳转到登录页
                        // 已经被拦截器拦截了,未登录
                        location.href="/login.html";
                    }
                }
            })
        }

第三步:写后端代码

步骤一:写mapper层

// 修改文章
    public int updateArticle(ArticleInfo articleInfo);

步骤二:映射配置文件.xml


        update articleinfo set title=#{title},description=#{description}, content=#{content} where aid=#{aid} and uid=#{uid}
    

步骤三:写service层

@Override
    public int updateArticle(ArticleInfo articleInfo) {
        return articleMapper.updateArticle(articleInfo);
    }

步骤四:写controller层 

/**
     * 修改文章
     * @param articleInfo
     * @param request
     * @return
     */
    @RequestMapping("/update")
    public Object updateArticle(ArticleInfo articleInfo,HttpServletRequest request){
        // 1.非空校验
        if (articleInfo==null||!StringUtils.hasLength(articleInfo.getTitle())
                ||!StringUtils.hasLength(articleInfo.getContent())
                ||articleInfo.getAid()<=0){
            return AjaxResult.fail(-1,"非法参数!");
        }
        // 2.组装数据
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo==null||userInfo.getUid()<=0){
            return AjaxResult.fail(-2,"请先登录!");
        }
        // 作者
        articleInfo.setUid(userInfo.getUid());
        String description = articleInfo.getContent();
        if (description.length()>MAX_DESCRIPTION_LENGTH){
            description=description.substring(0,MAX_DESCRIPTION_LENGTH);
        }
        // 简介
        articleInfo.setDescription(description);
        // 3.操作数据库
        int result = articleService.updateArticle(articleInfo);
        // 4.返回结果给前端
        if (result==1){
            return AjaxResult.success("修改成功!",1);
        }else{
            return AjaxResult.fail(-3,"修改失败!");
        }
    }

 4.8实现删除文章功能

删除博客功能需要用户登录了才能进行操作, 所以需要拦截. 删除博客时需要弹窗是否要删除, 防止用户误点. 并且只有我的博客列比页才有删除功能. 主页的博客列表页是没有的.

前端操作myblog_list.html

第一步:约定前后端接口

url : "/art/delete"
type : "POST"
data : aid
后端返回 code == 200 并且 data == 1 就弹出删除成功. 否则弹出删除失败.

第二步:写前端代码

// 删除博客
    function del(aid) {
        if (confirm('确定删除?')){
            jQuery.ajax({
                url:'/art/delete',
                type:'POST',
                data:{
                    aid:aid
                },
                success:function (body) {
                    if (body.code==200&&body.data==1){
                        // alert('删除成功!');
                        location.href=location.href;
                    }else{
                        alert('删除失败,请重试!');
                    }
                },
                error:function (err) {
                    if (err!=null&&err.status==401){
                        // alert("用户未登录, 即将跳转到登录页!")
                        // 已经被拦截器拦截了 - 未登录
                        location.href="/login.html";
                    }
                }
            })
        }
    }

第三步:写后端代码

步骤一:写mapper层

// 删除博客
public int deleteArticle(@Param("aid")Long aid,@Param("uid") Long uid);

步骤二:映射配置文件.xml


        delete from articleinfo where aid=#{aid} and uid=#{uid}
    

步骤三:写service层

@Override
    public int deleteArticle(Long aid, Long uid) {
        return articleMapper.deleteArticle(aid,uid);
    }

步骤四:写controller层

/**
     * 删除博客
     * @param aid
     * @param request
     * @return
     */
    @RequestMapping("/delete")
    public Object deleteArticle(Long aid,HttpServletRequest request){
        // 1.非空校验
        if (aid==null||aid<=0){
            return AjaxResult.fail(-1,"非法参数!");
        }
        // 2.获取登录用户
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo==null||userInfo.getUid()<=0){
            return AjaxResult.fail(-2,"请先登录!");
        }
        ArticleInfo articleInfo = articleService.getDetailByAid(aid);
        if (articleInfo==null){
            return AjaxResult.fail(-5,"查询不到该文章!");
        }
        if (articleInfo.getUid()!=userInfo.getUid()){
            return AjaxResult.fail(-4,"不能删除别人的文章!");
        }
        // 3.操作数据库
        int result = articleService.deleteArticle(aid, userInfo.getUid());
        if (result==1){
            return AjaxResult.success("删除成功!",1);
        }else{
            return AjaxResult.fail(-3,"删除失败");
        }
    }

4.9实现密码的加盐加密

加盐处理

加盐就是在每次加密之前, 给密码加上一个不同的盐值(32位UUID生成), 此时再对这个最终密码进行 md5 操作, 那么每次加密得到的 md5 值就不一样了. 于是, 想要解密就必须拿到最终的 md5 值和盐值, 才能解密出原始的密码.

示例 

SSM项目-博客系统_第12张图片

SSM项目-博客系统_第13张图片

使用Java提供的UUID每次生成不同的盐值,然后与密码拼接在一起生成不同的md5值,那么每次加密的密码肯定就不一样了 

加盐加密我们解决了,但是关键在于加密后的密码要拿什么对照去解密出原始密码

校验密码的有效性

1.待验证密码

2.盐值

既然知道了加密后的密码与盐值共同作用下才能解密,那么盐值也需要存储到数据库

盐值该怎么存储呢

1.新增一个字段来存放盐值

2.将盐值和密码存储在一个字段中(正确选择)

为什么不把盐值重新作为一个字段

首先违背了数据库的第三范式,因为盐值与表中业务逻辑无关

其次,如果单独把盐值存储一列,那么很容易让人根据规律猜想并破解出来

因此多角度考虑后选择将盐值和密码存储在一个字段中, 最终通过某种特征来将加密密码和盐值区分. 

上面所说的某种特征代表什么呢请看下图

SSM项目-博客系统_第14张图片

SSM项目-博客系统_第15张图片

从代码中及运行结果中可以看出如下特征 :

1. Java 提供的 UUID 生成的随机盐值, 如果替换中间的 "-" 之后 (让别人看不出是 UUID 的特征), 始终都是 32 位数据.
2. md5 加密后的数据, 不论密码的长短, 生成的加密密码也始终是 32 位数据. 

既然长度都是固定32位,那么就可以在长度上做文章了.

我们约定将盐值和$和最终的加密密码拼接在一起存储在数据库中. 也就是一个 65位的数据. (盐值在前 , 密码在后)

总结:做到这种程度的加密后, 你的密码就已经足够安全了, 就算黑客进行一个密码的破解, 至少也要花上一段时间,当同样的时间所带来的收益很少时候, 黑客自然就没有动力去做这些事情了.

加盐加密代码的真正实现

ublic class PasswordUtil {
    /**
     * 密码加盐操作 (格式:盐值32 $ 加密之后的密码)
     *
     * @param password 原密码
     * @return
     */
    public static String encrypt(String password) {
        // 生成盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 使用加密算法将盐值和原密码进行加密
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 将盐值和加密后的密码一起返回
        String dbPassword = salt + "$" + finalPassword;
        return dbPassword;
    }

    /**
     * 密码解密操作
     * @param inputPassword
     * @param dbPassword
     * @return
     */
    public static boolean decrypt(String inputPassword,String dbPassword){
        // 验证参数
        if (!StringUtils.hasLength(inputPassword)||!StringUtils.hasLength(dbPassword)
                ||dbPassword.length()!=65||!dbPassword.contains("$")){
            return false;
        }
        // 将用户输入的密码和数据库的盐值进行加密,得到待验证密码的加密密码

        // 得到盐值 $ 最终正确的密码
        String[] dbPasswordArray=dbPassword.split("\\$");
        String salt=dbPasswordArray[0];
        String finalPassword=dbPasswordArray[1];

        // 将数据库中的盐值加上用户输入的密码,进行加密 = 待验证的加密密码
        String userPassword=DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes());
        // 将待验证的加密密码和数据库的加密密码进行对比
        if (userPassword.equals(finalPassword)){
            return true;
        }
        // 将结果返回给调用方
        return false;
    }
}

加盐加密密码实现之后,前面注册和登录密码相关的功能也要修改了

4.10加盐之后注册功能的变化

加盐处理后, 我们存储数据库的密码, 就需要存储进行加密后的最终密码和盐值拼接在一起的结果了. (约定盐值在前, 密码在后)

controller层变化

/**
     * 注册功能实现
     * @param userInfoVO
     * @return
     */
    @RequestMapping("/reg")
    public Object reg(UserInfoVO userInfoVO) {
        // 1.非空校验
        if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())
                || !StringUtils.hasLength(userInfoVO.getPassword())
                || !StringUtils.hasLength(userInfoVO.getCheckCode())) {
            return AjaxResult.fail(-1, "非法参数请求!");
        }
        // todo:密码进行加盐
        userInfoVO.setPassword(PasswordUtil.encrypt(userInfoVO.getPassword()));
        // 2.进行数据库添加操作
        int result = userService.addUser(userInfoVO);
        if (result == 1) {
            return AjaxResult.success("注册成功!", 1);
        } else {
            return AjaxResult.fail(-1, "数据库添加出错!");
        }
    }

4.11加盐之后登录功能的变化

注册的时候, 我们存储的是加盐处理后的 65 位密码, 那么登录的时候, 验证密码用户名密码正确性的时候, 就不能像之前直接从数据库取出密码和前端传递过来的密码进行比较了. 做出以下调整

controller层变化

/**
     * 登录功能实现
     * @param userInfoVO
     * @param request
     * @return
     */
    @RequestMapping("/login")
    public int login(UserInfoVO userInfoVO, HttpServletRequest request) {
        // 1.非空校验
        if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())
                || !StringUtils.hasLength(userInfoVO.getPassword())
                || !StringUtils.hasLength(userInfoVO.getCheckCode())) {
            return 0;
        }
        // 2.查询操作
        // 2.1.通过用户名查找对象
        UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());
        // 2.2 如果对象为空或者为无效对象,则返回-1
        if (userInfo == null || userInfo.getUid() <= 0) {
            return -1;
        }
        // 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1
//        if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {
        if (PasswordUtil.decrypt(userInfoVO.getPassword(),userInfo.getPassword())) {
            return -1;
        } else {
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
            return 1;
        }
    }

4.12实现博客详情页

此处的博客详情页, 是不能被拦截器拦截的, 因为主页有一个没有登录就可以访问的所有人的分页列表页, 在用户没有登录的情况下也是可以查看别人的文章详情页的. 此处自己的详情页就是多了两个按钮 : 修改,删除. (前端操作 blog_content.html)

博客详情页要实现的功能

1. 展示博客详情内容.
2. 展示博客的访问量.
3. 展示左侧博客对应的作者的个人信息(头像,昵称和发表文章数量)

第一步:引入需要ajax请求的依赖

第二步:约定前后端接口

获取文章详情、展示个人信息

url : "/art/detail"
type : "GET"
data : aid - 文章 id (从URL中获取)
后端返回 200 , 并且 data 不为空就构造数据. 

更新文章的访问人数

url : "/art/rcount"
type : "POST"
data : aid - 文章 id (从URL中获取)

第三步:写前端代码

从 URL 中获取 id 的方法封装到独立的 js 中,因为经常要使用到它util.js

// 获取url某个参数的值
function getUrlParam(key) {
    // 查询字符串
    var params=location.search;
    // 判断字符串是否含有?
    if (params!=null&¶ms.indexOf("?")>=0){
        // 截取从下标1开始到结束的字符串
        params=params.substring(1);
        // 键值对之间使用&分割
        var paramArr=params.split("&");
        for(let i=0;i

获取博客详情以及个人信息

var aid = getUrlParam("aid");//得到文章id
    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 getArtDetail() {
        if (aid != null && aid > 0) {
            // 访问后端查寻文章详情
            jQuery.ajax({
                url: '/art/detail',
                type: 'GET',
                data: {"aid": aid},
                success: function (body) {
                    if (body.code == 200 && body.data != null) {
                        // 填充标题
                        let title = document.querySelector('#title');
                        title.innerHTML = body.data.title;
                        // 填充日期
                        let createtime = document.querySelector('#createtime');
                        createtime.innerHTML = formatDate(body.data.createtime);
                        // 填充访问量
                        let rcount = document.querySelector('#rcount');
                        rcount.innerHTML = body.data.rcount+1;//为了和数据库保持相同  
                        // 填充内容
                        initEdit(body.data.content);
                        // 左侧个人信息
                        if (body.data.photo!=null&&body.data.photo!=""){
                            let photo=document.querySelector("#photo");
                            photo.src=body.data.photo;
                        }
                        let nickname=document.querySelector("#nickname");
                        nickname.innerHTML=body.data.nickname;
                        let artCount=document.querySelector("#artCount");
                        artCount.innerHTML=body.data.artCount;
                    }
                }
            });
        }
    }
    getArtDetail();

更新访问人数 

// 更新访问量
    function updateCount() {
        jQuery.ajax({
            url: '/art/rcount',
            type: 'POST',
            data: {
                "aid": aid
            },
            success: function (res) {
            }
        })
    }

    updateCount();

 第四步:写后端代码

查询 : 查询文章详情, 查询作者的信息(头像,昵称,文章数量)
修改 : 每次访问, 都让访问量 + 1.

步骤一:mapper层ArticleMapper

// 获取文章详情(多表联查)
    ArticleInfoVO getDetail(Long aid);

    // 更新访问人数
    int updateRCount(Long aid);

步骤二:对应的xml实现



    
        update articleinfo set rcount=rcount+1 where aid=#{aid}
    

步骤三:service层

@Override
    public ArticleInfoVO getDetail(Long aid) {
        return articleMapper.getDetail(aid);
    }

    @Override
    public int updateRCount(Long aid) {
        return articleMapper.updateRCount(aid);
    }

步骤四:controller层

/**
     * 获取文章详细信息(查看详情)
     * @param aid
     * @return
     */
    @RequestMapping("/detail")
    public Object getDetail(Long aid){
        // 1.参数校验
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        // 2.查询数据库中的文章信息
        ArticleInfoVO articleInfoVO = articleService.getDetail(aid);
        if (articleInfoVO == null || articleInfoVO.getAid() <= 0) {
            return AjaxResult.fail(-2, "文章查询失败!");
        }
        // 3.组装数据,查询当前用户总共发表的文章数
        int count = articleService.myArtCount(articleInfoVO.getUid());
        articleInfoVO.setArtCount(count);
        // 4.将最终组装好的数据返回给前端
        return AjaxResult.success(articleInfoVO);
    }


    /**
     * 更新阅读人数
     *
     * @param aid
     * @return
     */
    @RequestMapping("/rcount")
    public Object updateRCount(Long aid) {
        // 参数校验
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1, "参数有误!");
        }
        // 修改数据库
        int result = articleService.updateRCount(aid);
        // 将结果返回给前端
        return AjaxResult.success(result);
    }

5.扩展功能

5.1验证码实现功能

实现思路

1.生成验证码

2.将本地验证码发布成url

3.后端返回验证码的url给前端

4.前端将用户输入的密码发送给后端

5.后端校验验证码

5.1.1生成验证码

步骤一:添加 hutool 验证码依赖(顺便加上redis)


            org.springframework.session
            spring-session-data-redis
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            cn.hutool
            hutool-all
            5.8.16
        

步骤二:创建验证码的控制器

@RestController
public class CaptchaController {
    @Value("${imagepath}")
    private String imagepath;

    // 1.生成验证码到本地
    // 定义图形验证码的长和宽
    LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);
    String uuid = UUID.randomUUID().toString().replace("-", "");

    // 图形验证码写出,可以写出到文件  (也可以写出到流)
    lineCaptcha.write(imagepath + uuid + ".png");

    return AjaxResult.success(imagepath+uuid+".png");
}

application.propertities 中添加验证码保存路径

# 图片保存路径
imagepath=E:\\image\\

注意事项

如果项目配置了拦截器,一定要让拦截器给该获取验证码接口放行

使用 UUID 每次生成不同地址的验证码

步骤三:前端验证码代码

验证码   

项目启动访问127.0.0.1:8080/getcaptcha

SSM项目-博客系统_第16张图片SSM项目-博客系统_第17张图片

5.1.2将本地验证码发布成url

步骤一:配置映射图片路径

@Configuration
public class AppConfig implements WebMvcConfigurer {
 
    @Value("${imagepath}")
    private String imagepath;
 
    /**
     * 映射图片路径
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/image/**")
                .addResourceLocations("file:" + imagepath);
    }
}

注意事项:如果项目配置了拦截器,一定要让拦截器给/image/**接口放行

如果没有配置这个映射图片路径,后续就会发生以下情况,图片显示不出来

SSM项目-博客系统_第18张图片

步骤二:使用映射后的网络路径访问验证码

浏览器输入:http://127.0.0.1:8080/image/1d47179e5d26445d87b0e52fec326877.png

SSM项目-博客系统_第19张图片

5.1.3后端返回验证码的 URL 给前端

后端不仅要返回验证码给前端, 还需要返回一个 "验证码的 key" 给前端

后端返回验证码的 URL 给前端可以理解, 前端需要展示给用户看; 

那么验证码的 key 是为什么呢

后端需要将验证码存储 redis, 因为在某一时间内可以生成很多验证码, 而用户输入的正确与否, 需要在后端进行判断, 后端进行判断时, 就得把生成的验证码存储到redis ,而存储 redis 我们可以借着前面生成的 UUID , 把 UUID 作为 key, 验证码作为 value 去存储. 然后再将 key 去传给前端, 前端就可以带着输入的验证码和 key 一起传给后端, 后端就可以拿着 key作为条件从redis找到相应的一个验证码, 然后与前端传过来的即用户输入的验证码,两者相互比较就行了.

CaptchaController.java 代码完整版

@RestController
public class CaptchaController {
    @Value("${imagepath}")
    private String imagepath;

    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping("/getcaptcha")
    public Object getCaptcha() {
        // 1.生成验证码到本地
        // 定义图形验证码的长和宽
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);
        String uuid = UUID.randomUUID().toString().replace("-", "");

        // 图形验证码写出,可以写出到文件  (也可以写出到流)
        lineCaptcha.write(imagepath + uuid + ".png");
        // url地址
        String url = "/image/" + uuid + ".png";
        // 将验证码存储到redis
        redisTemplate.opsForValue().set(uuid, lineCaptcha.getCode());
        HashMap result = new HashMap<>();
        result.put("codeUrl", url);
        result.put("codeKey", uuid);
        return AjaxResult.success(result);
    }
}

5.1.4前端将用户输入的验证码传给后端

前端访问页面时刷新验证码

// 验证码key
        var codeKey = "";

        // 获取并显示验证码
        function refreshCode() {
            jQuery.ajax({
                url:'/getcaptcha',
                type:'GET',
                data:{},
                success:function (body) {
                    if(body.code==200&&body.data!=null&&body.data!=""){
                        // 获取验证码成功
                        codeKey=body.data.codeKey;
                        jQuery('#codeimg').attr('src',body.data.codeUrl);
                    }
                }
            })
        }

        refreshCode();

前端将验证码和 key 传给后端

function doLogin() {
            // 非空参数的校验
            var username=jQuery('#username')
            var password=jQuery('#password')
            var checkCode=jQuery('#code_input')

            if(username.val().trim()==''){
                alert('请输入用户名!')
                username.focus()
                return false
            }
            if(password.val().trim()==''){
                alert('请输入密码!')
                password.focus()
                return false
            }
            if(checkCode.val().trim()==''){
                alert('请输入验证码!')
                checkCode.focus()
                return false
            }

            // 2. 发送请求给客户端
            jQuery.ajax({
                url:"/user/login",
                type:"post",
                data:{
                    codeKey:codeKey,
                    // 前面的 username 可以加引号, 也可以不加
                    username:username.val(),
                    password:password.val(),
                    checkCode:checkCode.val()
                },
                success:function(body) {
                    if(body.code == 200 && body.data == 1) {
                        // alert("登录成功!");
                        location.href = "myblog_list.html";
                    } else {
                        alert("用户名或密码错误, 请重新输入!");
                        username.focus();
                    }
                }
            });
        }

5.1.5后端校验验证码

这里拿登录来举例,注册也是同样的道理,就不一一进行展示了

/**
     * 登录功能实现
     * @param userInfoVO
     * @param request
     * @return
     */
    @RequestMapping("/login")
    public int login(UserInfoVO userInfoVO, HttpServletRequest request) {
        // 1.非空校验
        if (userInfoVO == null || !StringUtils.hasLength(userInfoVO.getUsername())
                || !StringUtils.hasLength(userInfoVO.getPassword())
                || !StringUtils.hasLength(userInfoVO.getCheckCode())
                ||!StringUtils.hasLength(userInfoVO.getCodeKey())) {
            return 0;
        }
        // redis 里面 key 对应的真实的验证码
        String redisCodeValue = (String) redisTemplate.opsForValue().get(userInfoVO.getCodeKey());
        // 验证码错误
        if (!StringUtils.hasLength(redisCodeValue) || !redisCodeValue.equals(userInfoVO.getCheckCode())) {
            return -1;
        }
        // 验证码正确,清除当前验证码
        redisTemplate.opsForValue().set(userInfoVO.getCodeKey(), "");
        // 2.查询操作
        // 2.1.通过用户名查找对象
        UserInfo userInfo = userService.getUserByUsername(userInfoVO.getUsername());
        // 2.2 如果对象为空或者为无效对象,则返回-1
        if (userInfo == null || userInfo.getUid() <= 0) {
            return -1;
        }
        // 2.3 如果用户输入的密码与数据库输入密码不一致,则返回-1
//        if (!userInfoVO.getPassword().equals(userInfo.getPassword())) {
        if (!PasswordUtil.decrypt(userInfoVO.getPassword(),userInfo.getPassword())) {
            return -1;
        } else {
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
            return 1;
        }
    }

5.2博客详情页(评论功能)实现

实现思路

1.初始化评论列表

2.发表评论

3.删除评论

评论文本框下方如果用户未登录,显示请先登录;如果用户登录了,显示用户昵称

5.2.1初始化评论列表

  • 加载博客下面的评论(功能一)
  • 加载当前登录人的名字到评论框下面 (登录了才能评论)(功能二)

操作blog_content.html 

第一步:约定前后端接口

获取评论列表 

url:"/comment/list",

type:"GET",

data:{aid:aid},

后端返回 200 , 并且 data 不为空就构造数据. 

 获取用户登录信息 

url:"/user/info",

type:"GET",

data:{}

后端返回 200 , 并且 data 不为空以及返回用户uid>0就构造数据. 

判断当前文章是否是当前用户发表的

url:"/user/isartbyme",

type:"GET",

data:{aid:aid}

后端返回 200 , 并且 data ==1就显示删除按钮 

第二步:写前端代码


    

| 访问量:

评论

请先登录

// 获取评论列表
    function getCommentList() {
        jQuery.ajax({
            url:"/comment/list",
            type:"GET",
            data:{
                "aid":aid
            },
            success: function (body) {
                if (body.code == 200 && body.data != null) {
                    var commentListHtml = "";
                    for (let i = 0; i < body.data.length; i++) {
                        var comment = body.data[i];
                        commentListHtml += '
'; commentListHtml += comment.nickname + ':' + comment.content; commentListHtml += '      '; commentListHtml += ''; commentListHtml += "
"; } jQuery('#commentlist').html(commentListHtml); var commentCount = body.data.length; jQuery("#commentCount").html('共 ' + commentCount + ' 条评论'); } } }); }

此处要给删除按钮添加 display: none 样式, 后续在 getLoginUser() 方法中会做判断, 判断当前登录人和文章作者是否为同一个人, 如果是则显示删除按钮, 如果不是, 则不显示删除按钮. 

第三步:写后端代码

步骤一:mapper层

// 获取评论列表
    List getList(@Param("aid") Long aid);

步骤二:对应的xml实现

步骤三:service层

@Override
    public List getList(Long aid) {
        return commentMapper.getList(aid);
    }

步骤四:controller层

/**
     * 获取评论列表
     * @param aid
     * @return
     */
    @RequestMapping("/list")
    public Object getCommentList(Long aid) {
        // 1. 参数效验
        if(aid==null || aid<=0) {
            return AjaxResult.fail(-1, "参数有误!");
        }
        // 2. 查询数据库
        List list = commentService.getList(aid);
        // 3. 就结果返回给前端
        return AjaxResult.success(list);
    }

后端接口 /comment/list 记得要在拦截器里边放行, 如果拦截了, 那么用户不登录, 就看不到评论了,而我们想要的效果是: 就算用户不登录, 所有人的的博客列表页查看文章详情的时候, 也能看到评论, 只是不管文章归属人是谁都不会显示删除按钮

功能二的实现

第一步:写前端代码 

var isLogin=false;
// 获取当前用户登录信息
    function getLoginUser() {
        jQuery.ajax({
            url: '/user/info',
            type: 'GET',
            data: {},
            success: function (body) {
                if (body.code == 200 && body.data != null && body.data.uid >= 0) {
                    // 当前用户登录了
                    isLogin = true;
                    jQuery('#comment_login_name').html(body.data.nickname);
                    // 判断当前文章是否是当前用户发表的
                    isArticleByMe(aid);
                } else {
                    // 当前用户未登录
                }
            }
        })
    }

    // 判断当前文章是否是当前用户发表的
    function isArticleByMe(aid) {
        jQuery.ajax({
            url: '/user/isartbyme',
            type: 'GET',
            data: {
                "aid": aid
            },
            success: function (body) {
                if (body.code == 200 && body.data == 1) {
                    // 当前文章归属于当前登录用户
                    jQuery('.comment_del_class').each(function (i) {
                        jQuery(this).show();
                    })
                }
            }
        })
    }

getLoginUser() 方法的作用是控制登录了才能发表评论,未登录不能发表评论.

isArticleByMe()  方法的作用是控制是否显示删除按钮.

【注意】此处最好将 isArticleByMe() 方法写在 getLoginUser()中 ajax 最后, 如果写在外面, 会出现这样一个问题 : 

" 因为同一个页面下面的 ajax  请求的执行顺序是不一定的, 那么就有可能先执行了控制是否显示删除按钮的 ajax, 再执行 getLoginUser(), 如果是这样, 那么这篇文章属不属于当前登录人, 都不会显示删除按钮, 没有登录怎么判断文章归属人是吧."所以 isArticleByMe() 方法要在 getLoginUser() 方法执行后再调用.

第二步:写后端代码

/**
     * 获取个人信息(博客详情页)
     * @param request
     * @return
     */
    @RequestMapping("/info")
    public Object info(HttpServletRequest request) {
        // 从 session 工具类中拿用户登录信息
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }
        return AjaxResult.success(userInfo);
    }

    /**
     * 判断文章是否是当前登录人所写的
     * @param aid
     * @param request
     * @return
     */
    @RequestMapping("/isartbyme")
    public Object isArticleByMe(Long aid, HttpServletRequest request) {
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1, "参数有误! ");
        }
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录! ");
        }
        ArticleInfo articleInfo = articleService.getDetailByAid(aid);
        if (articleInfo != null && articleInfo.getAid() >= 0
                && articleInfo.getUid() == userInfo.getUid()) {
            // 文章归属于当前登录人
            return AjaxResult.success(1);
        }
        return AjaxResult.success(0);
    }

特别注意:此处的 /user/info 和 /user/isartbyme 接口都需要在拦截器里边放行.  

5.2.2实现发表评论功能

第一步:约定前后端接口

url:"/comment/add",

type:"POST",

data:{aid,content},

结果返回200且data==1就构造数据

第二步:写前端代码

// 发表评论
    function addComment() {
        // 评论正文
        var comment_content = jQuery('#comment_content');
        // 非空判断
        if (comment_content.val().trim() == "") {
            alert('请先输入评论的内容!');
            comment_content.focus();
            return false;
        }
        // 判断是否登录
        if (!isLogin) {
            alert('请先登录!');
            return false;
        }
        // 将前端数据发送给后端(文章id、评论内容)
        jQuery.ajax({
            url: '/comment/add',
            type: 'POST',
            data: {
                "aid": aid,
                "content": comment_content.val()
            },
            success: function (body) {
                // 将后端返回的数据显示给用户
                if (body.code == 200 && body.data == 1) {
                    // 评论添加成功
                    alert('评论发表成功!');
                    // 刷新评论
                    location.href = location.href;
                } else {
                    alert('抱歉,发表失败,请重试!' + body.msg);
                }
            }
        })


    }

第三步:写后端代码

步骤一:mapper层

// 发表评论
    boolean addComment(CommentInfo commentInfo);

步骤二:对应的xml实现


        insert into commentinfo values (#{cid},#{aid},#{uid},#{content},#{createtime});
    

步骤三:service层

@Override
    public boolean addComment(CommentInfo commentInfo) {
        return commentMapper.addComment(commentInfo);
    }

步骤四:写controller层

/**
     * 发表评论
     *
     * @param aid
     * @param content
     * @param request
     * @return
     */
    @RequestMapping("/add")
    public Object addComment(Long aid, String content, HttpServletRequest request) {
        // 1.参数校验
        if (aid == null || aid <= 0 || !StringUtils.hasLength(content)) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        // 2.组装数据(将 uid 从session中获取出来)
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录");
        }
        CommentInfo commentInfo = new CommentInfo();
        commentInfo.setAid(aid);
        commentInfo.setUid(userInfo.getUid());
        commentInfo.setContent(content);
        // 3.将评论的对象存入数据库
        boolean result = commentService.addComment(commentInfo);
        // 4.将数据执行的结果返回给前端
        return AjaxResult.success(result == true ? 1 : 0);
    }

发表评论的路由 /comment/add 需要在拦截器里边配置拦截, 因为只有登录了才能发表评论,即不需要放行

5.2.3实现删除评论功能

第一步:约定前后端接口

url:"/comment/del",

type:"POST",

data:{aid,content},

结果返回200且data==1就构造数据

第二步:写前端代码

// 评论删除功能
    function del(cid) {
        if (!confirm('确定删除?')){
            return false;
        }
        // 1.参数校验
        if (cid == "" || cid <= 0) {
            alert('抱歉,操作失败,请刷新页面之后重试!');
            return false;
        }
        if (aid == "" || aid <= 0) {
            alert('抱歉,评论删除失败,请刷新页面之后重试!');
            return false;
        }
        // 2.发送数据给后端(评论的id,文章的id)
        jQuery.ajax({
            url: '/comment/del',
            type: 'POST',
            data: {
                "aid": aid,
                "cid": cid
            },
            success: function (body) {
                // 3.将后端结果显示给用户
                if (body.code == 200 && body.data == 1) {
                    // 删除成功
                    alert('恭喜,评论删除成功!')
                    location.href = location.href;
                } else {
                    alert('抱歉,评论删除失败!' + body.msg);
                }
            }
        })


    }

想要删除评论, 那么至少得传递两个参数给后端, 一个是 cid (评论 ID), 一个是 aid (文章 ID), cid 在前边加载评论列表的时候, 已经在返回数据 body 中的 comment 对象中拿到了, 而aid呢 ,前面已经调用了工具方法 getUrlParam 获取并保存 aid 全局变量中, 因此也可以直接拿到.

【注意】虽然是否能够删除评论需要拿着 aid 查询出具体的 articleinfo 对象, 再拿着这个对象的 uid 和登录人的 uid 进行比较, 相同才可以删除评论. 但是此处不能将 uid (用户人的 ID) 通过 ajax 发给后端, 一旦 uid 通过参数来接受登录人的 ID 了, 那么就有被篡改的风险, 别人可以写一个接口绕过你的 ajax 直接访问后端接口 (例如: postman), 这样就非常不安全, 所以 uid 最好从后端的 session 中获取.

第三步:写后端代码

步骤一:写mapper层

// 删除评论
    boolean deleteCommentById(@Param("cid") Long cid);

步骤二:对应的xml实现


        delete from commentinfo where cid=#{cid}
    

步骤三:写service层

@Override
    public boolean deleteCommentById(Long cid) {
        return commentMapper.deleteCommentById(cid);
    }

步骤四:写controller层

/**
     * 删除评论
     *
     * @param aid
     * @param cid
     * @return
     */
    @RequestMapping("/del")
    public Object delComment(Long aid, Long cid, HttpServletRequest request) {
        // 1.参数校验
        if (aid == null || aid <= 0 || cid == null || cid <= 0) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        // 2.校验权限(判断当前文章是否属于当前登录用户写的)
        // 2.1获取当前登录用户
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录!");
        }
        // 2.2获取当前文章id
        ArticleInfo articleInfo = articleService.getDetailByAid(aid);
        if (articleInfo == null && articleInfo.getAid() <= 0) {
            return AjaxResult.fail(-3, "非法的文章id!");
        }
        // 2.3比较文章归属的用户和当前登录的用户是否一致
        if (userInfo.getUid() != articleInfo.getUid()) {
            return AjaxResult.fail(-4, "非法操作!");
        }
        // 3.删除评论(操作数据库)
        boolean result = commentService.deleteCommentById(cid);
        // 4.将结果返回给前端
        return AjaxResult.success(result == true ? 1 : 0);
    }

删除评论的路由 /comment/del 需要在拦截器里边配置拦截, 因为只有登录了才能删除评论,即不需要放行

5.3个人中心功能实现

5.3.1修改用户头像

需要实现的功能有:

1.获取原来的用户头像和用户名

2.保存头像

功能一这样处理的原因是:想要修改头像, 那么就得先获取数据库中原来的头像, 此处顺便将原用户名一起获取并展示出来. 原密码最好不要获取出来, 因为有可能你在修改密码的中途有事情要处理,如果这时有人刚好看到你密码那么别人就有机可乘修改你的信息了,所以我们最好不用把原密码获取出来

操作myinfo.html

第一步:约定前后端接口

url:"/user/info",

type:"GET",

data:{},

返回的结果code==200且数据不为空以及用户uid>0,则构造数据,否则弹窗显示错误信息

第二步:写前端代码

// 获取用户头像和用户名
        function initPage() {
            jQuery.ajax({
                url:"/user/info",
                type:"GET",
                data:{},
                success:function(body) {
                    if(body.code==200 && body.data!=null && body.data.uid>=0) {
                        // 得到了当前的 userinfo
                        var userinfo = body.data
                        if(userinfo.photo!=null && userinfo.photo!="") {
                            jQuery("#photo").attr("src",userinfo.photo);
                        }
                        jQuery("#username").val(userinfo.username);
                    } else {
                        alert("抱歉: 查询用户信息出错, 请刷新页面再试! " + body.msg);
                    }
                }
            });
        }
        initPage();

1. 获取当前登录人的用户名和头像, 只需在后端的 session 中拿到具体的 userinfo 即可,所以不需要传参数uid到后端

2. success 回调函数中的注意事项:  因为我们在构造数据 (userinfo) 的时候, 头像一般都是写死的本地图片, 所以新用户一般都是默认头像, 所以我们在构造 photo 的 src 属性时, 一定要判断 photo 是否为 null 或者是否为 "", 如果是就不要设置 photo 的 src 属性, 否则会导致用户没有头像. 

第三步:写后端代码

/**
     * 获取个人信息(博客详情页、个人信息页)
     *
     * @param request
     * @return
     */
    @RequestMapping("/info")
    public Object info(HttpServletRequest request) {
        // 从 session 工具类中拿用户登录信息
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }
        return AjaxResult.success(userInfo);
    }

功能二保存头像

我们上传新的头像后, 并点击保存按钮时, 就是修改头像成功了

第一步:约定前后端接口 

url:"/user/save_photo",

type:"POST",

data:{},

返回的结果code==200且数据不为空,则构造数据,否则弹窗显示错误信息

第二步:写前端代码

// 保存头像
        function savePhoto() {
            // 得到图片
            var photo = jQuery("#file")[0].files[0];
            if (photo == null) {
                alert("请先选择要上传的头像!");
                return false;
            }
            // 构建一个 form 表单
            var formData = new FormData();
            formData.append("file", photo);
            jQuery.ajax({
                url:"/user/save_photo",
                type:"POST",
                data:formData,
                processData:false, // 告诉 jQuery 不要去加工数据
                contentType:false, // 告诉 jQuery 不要设置类型
                success:function(body) {
                    if(body.code==200 && body.data!=null && body.data!="") {
                        jQuery("#photo").attr("src",body.data);
                    } else {
                        // 图片上传失败
                        alert("抱歉: 上传图片失败, 请重试! " + body.msg);
                    }
                }
            });
        }

1. 此处的得到图片代码比较特殊

2. 发送 ajax 时, 参数是发送一个 form 表单给后端, 并携带两个参数 : processData 和 contentType设置属性(疑点)

3. 表单传给后端时, 后端针对图片生成一个网络地址映射到本地保存的地址, 然后将网络地址返回给前端, 前端将其设置给 photo 的 src 属性.

第三步:写后端代码

步骤一:映射图片路径

在配置文件中application.properties设置头像保存的本地路径(这里跟验证码保存路径相同,所以如果前面已经配置了可以跳过)

# 图片保存路径
imagepath=E:\\image\\

在全局配置类App.config类中添加映射图片路径方法

/**
     * 映射图片路径
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/image/**").addResourceLocations("file:" + imagepath);
    }

步骤二:保存图片到服务器, 保存图片地址到数据库

mapper层 

// 更新头像
    int updatePhoto(@Param("uid") Long uid,@Param("photo") String imgUrl);

 UserMapper.xml


        update userinfo set photo=#{photo} where uid=#{uid}
    

service层

@Override
    public int updatePhoto(Long uid, String imgUrl) {
        return userMapper.updatePhoto(uid,imgUrl);
    }

controller层 

/**
     * 个人中心:保存头像
     *
     * @param file
     * @param request
     * @return
     */
    @RequestMapping("/save_photo")
    public Object savePhoto(MultipartFile file, HttpServletRequest request) {
        // 1.保存图片到服务器
        // 得到图片的后缀
        String imageType = file.getOriginalFilename().substring(
                file.getOriginalFilename().lastIndexOf("."));
        // 生成图片名称
        String imgName = UUID.randomUUID().toString()
                .replace("-", "") + imageType;
        try {
            file.transferTo(new File(imagePath + imgName));
        } catch (IOException e) {
            return AjaxResult.fail(-1, "图片上传失败!");
        }
        String imgUrl = "/image/" + imgName;
        // 2.将图片地址保存到数据库
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            // 请先登录
            return AjaxResult.fail(-2, "请先登录!");
        }
        int result = userService.updatePhoto(userInfo.getUid(), imgUrl);
        if (result == 1) {
            // 更新头像到session中
            userInfo.setPhoto(imgUrl);
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
            return AjaxResult.success(imgUrl);
        } else {
            return AjaxResult.fail(-3, "数据库修改失败!");
        }
    }

1. 保存图片到服务器

当项目部署云服务器时, 用户访问项目并修改头像时, 是从用户的电脑上选取了一张图片上传到后端, 那么后端需要将这张图片保存到云服务器的一个本地路径, 然后再生成图片对应的网络地址. 最后将新头像对应的网络地址返回给前端, 前端就可以通过设置头像 photo 对应的 src 属性为返回的网络地址.

2. 保存图片到数据库

修改头像不仅要更新当前页面展示的头像, 数据库中的头像对应的网络地址也要更新, 另外如果更新数据成功了, 要同时更新 session 中的 userinfo 信息. 因为博客列表页的当前用户的身份信息, 后端都是从 session 中取出来返回给前端的, 所以需要同时更新数据库和 session 中的 photo 字段(属性).

5.3.2修改用户名或者密码

此处我们点击修改个人信息跳转到用户信息的页面时, 它只是将头像和用户名展示出来了, 而原密码, 新密码和确认密码需要手动输入(要么三个都不为空 - 改, 要么都为空 - 不改).

两种情况:

一:如果不修改密码, 只是修改用户名或者都不修改, 然后点击修改按钮, 就提示修改成功, 并跳转到我的内容管理页面.
二:如果修改了密码, 并点击了修改按钮, 就提示修改成功, 并强制用户重新登录.

第一步:约定前后端接口

url:"/user/update",

type:"POST",

data:{

      username,oldPassword,newPassword,isUpdatePassword

},

返回的结果code==200且data==1,则构造数据(如果isUpdatePassword为true,跳转到登录页面,反之则跳转到我的博客列表页面)

否则弹窗显示错误信息

第二步:写前端代码 

// 修改用户个人信息
        function updateUser() {
            var isUpdatePassword = false; // 是否修改密码
            // 1.非空效验
            var username = jQuery("#username");
            var oldPassword = jQuery("#old_password");
            var password = jQuery("#password");
            var password2 = jQuery("#password2");
            if (username.val().trim() == "") {
                alert("请先输入新用户名!");
                username.focus();
                return false;
            }
            if (oldPassword.val() != "" ||
                password.val() != "" || password2.val() != "") {
                // 需要修改密码
                isUpdatePassword = true;
                if (oldPassword.val().trim() == "") {
                    alert("请先输入原密码!");
                    oldPassword.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/update",
                type: "POST",
                data: {
                    "username": username.val(),
                    "oldPassword": oldPassword.val(),
                    "newPassword": password.val(),
                    "isUpdatePassword": isUpdatePassword
                },
                success: function (body) {
                    // 3.将返回的结果展现给用户
                    if (body.code == 200 && body.data == 1) {
                        // 修改成功
                        if (isUpdatePassword) {
                            alert("修改成功,请重新登录!");
                            // 修改密码,重新登录
                            location.href = "login.html";
                        } else {
                            alert("修改成功!");
                            location.href = "myblog_list.html";
                        }
                    } else {
                        // 修改失败
                        alert("抱歉:修改失败,请重试!" + body.msg);
                    }
                }
            });
        }

第三步:写后端代码

mapper层

// 根据id查询用户
    UserInfo getUserById(@Param("uid") Long uid);

    // 修改用户信息(用户名和密码)
    boolean updateUser(UserInfo userInfo);

UserMapper.xml



    
        update userinfo set username=#{username},password=#{password} where uid=#{uid}
    

service层

@Override
    public UserInfo getUserById(Long uid) {
        return userMapper.getUserById(uid);
    }

    @Override
    public boolean updateUser(UserInfo userInfo) {
        return userMapper.updateUser(userInfo);
    }

controller层 

/**
     * 个人中心:修改用户名或密码
     *
     * @param username
     * @param oldPassword
     * @param newPassword
     * @param isUpdatePassword
     * @param request
     * @return
     */
    @RequestMapping("/update")
    public Object update(String username, String oldPassword, String newPassword, Boolean isUpdatePassword, HttpServletRequest request) {
        // 1.参数校验
        if (!StringUtils.hasLength(username)) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        // 是否需要修改密码
        if (isUpdatePassword) {
            // 修改原密码
            if (!StringUtils.hasLength(oldPassword) || !StringUtils.hasLength(newPassword)) {
                return AjaxResult.fail(-1, "非法参数!");
            }
        }
        // 2.组装数据(从session获取用户)
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录!");
        }
        // 如果需要修改密码
        if (isUpdatePassword) {
            // 判断用户输入的旧密码和数据库的密码是否一致
            UserInfo dbUser=userService.getUserById(userInfo.getUid());
            boolean checkPassword = PasswordUtil.decrypt(oldPassword,dbUser.getPassword());
            if (!checkPassword) {
                return AjaxResult.fail(-3, "原密码输入错误!");
            }
            // 修改密码
            newPassword=PasswordUtil.encrypt(newPassword);
            userInfo.setPassword(newPassword);
        }
        // 3.修改数据库
        userInfo.setUsername(username);
        boolean result = userService.updateUser(userInfo);
        // 更新 session 中的用户名
        if(result) {
            userInfo.setUsername(username);
            HttpSession session = request.getSession();
            session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);
        }
        // 4.将结果返回给前端
        return AjaxResult.success(result ? 1 : 0);
    }

1. 非空效验

        前端传递了新用户名, 原密码, 新密码, 以及是否修改了密码的标志, 于是在做判断时, 如果只修改了用户名, 就可以使用 isUpdatePassword 标志位来跳过接下来更新数据密码的操作. 否则都要进行修改.

2. 组装数据

       组装好一个新的 userinfo (新的用户名或密码), 为更新数据库操作提供数据源, 此处更新密码成功的前提是原密码和数据库密码要保持一致, 而数据库中存储的是加密后的密码, 所以需要先拿着原密码和数据库中的密码去调用解密方法去比较, 如果匹配成功,就可以进行修改密码操作,反之,则返回错误信息(原密码不正确)

【注意】session 中的对象存储机制 >>

      由于我们是可以拿到当前用户的  session, 所以想要拿数据库中存储的密码, 我就会想着去 session 中拿到 userinfo, 再去拿到对应的密码, 这确实挺方便. 但是我在实现登录页面时, 登录成功后并将 session 存储 redis, 但是在返回数据给前端之前, 我执行了将密码置为空字符串这一操作, 因为密码如果通过网络传输返回给前端, 是不安全的. 

        <问题的出处>  正因为我的这一步置空字符串操作, 就会导致 session 中的密码也变成了空字符串. 这是为什么呢 ??

因为 session 的底层是用 concurrentHashMap 来保存数据的, 而 map 中并没有直接存储新的对象, 而是存储了对象的引用, 也就是 userinfo 的引用, 虽然我是在设置密码为空之前就将 userinfo 存储 session 了, 但是这也同样影响了 session 中的 password 了. 此时 session 中的 password 已经是空字符串了.

       再回到调用解密方法这一步, 我们就不能拿着原密码和 session 中的密码去调用解密码方法了, 而是需要拿着从 session 获取到的 userinfo 中的用户 id, 去查数据库得到一个新的 userinfo, 此时这个 userinfo 的密码才不为空, 才可以拿着它的 password 去和原密码去调用解密方法.

3. 修改数据库

        经过了第二步的组装数据, 第三步就变得简单了, 我们可以自己写个修改方法将userinfo对象传递过去,根据uid修改用户名和密码就可以了

       另外就是修改完数据库之后, 要及时更新 session 中的用户名, 因为如果只修改了用户名, 不修改密码, 修改完成后会跳转博客列表页, 而博客列表页的用户身份信息都是从后端的 session 中来的, 如果不及时更新 session 的话, 那么在你下次重新登录之前, 用户名都不会变.

6.部署环境

mysql 

SSM项目-博客系统_第20张图片

redis

SSM项目-博客系统_第21张图片

SSM项目-博客系统_第22张图片

在部署环境时遇到问题:

nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
### The error may exist in class path resource [mapper/ArticleMapper.xml]
### The error may involve com.huhu.mapper.ArticleMapper.getList
### The error occurred while executing a query
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed 

大概意思就是:不允许公钥检索(个人不理解)后来查找资料:就在配置文件加上属性

allowPublicKeyRetrieval=true就解决了

你可能感兴趣的:(spring,spring,boot)