项目——博客系统

时间:2023-02-18 10:52:26


前言
项目——博客系统
这个博客系统前端分为8个页面,分别是注册页,登录页,编辑页,修改页,个人主页,博客正文页,草稿列表页,博客列表页

项目优点

  1. 框架:使用ssm(SpringBoot + SpringMVC + MyBatis)
  2. 密码:用户登录用的密码是使用加盐算法处理然后存储到数据库
  3. 用户登录状态持久化:将session持久化到redis
  4. 功能升级:在博客列表页实现一个分页功能
  5. 使用拦截器进行用户的登录验证,统一数据返回格式,统一异常处理

项目创建

创建一个SpringBoot项目,添加需要的依赖

<dependencies>
		<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>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.3.0</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

创建相应的目录,文件,表,导入前端资源

首先在数据库建表,这里直接提供sql语句

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

-- 使用数据数据
use mycnblog;

-- 创建表[用户表]
drop table if exists  userinfo;
create table userinfo(
    id int primary key auto_increment,
    username varchar(100) not null,
    password varchar(32) not null,
    photo varchar(500) default '',
    createtime datetime default now(),
    updatetime datetime default now(),
    `state` int default 1
) default charset 'utf8mb4';

-- 创建文章表
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', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);

-- 文章添加测试数据
insert into articleinfo(title,content,uid,state)
    values('喜讯','今天是正月初六',9,2);

  1. 实体层(model):创建实体类UserInfo,ArticleInfo
  2. 控制层(controller):创建控制器UserController和ArticleInfo
  3. 服务层(servlce):创建服务类:UserService和ArticleService
  4. 持久层(mapper):创建接口UserMapper和ArticleMapper,并创建对应的xml文件,在yml文件里配置好
  5. 工具层(common):统一返回类(ResponseAdvice,AjaxResult等)

注意:创建完相应的类就得将需要加的注解加上去,这里就不一一展示了
创建完后的目录结构
项目——博客系统

yml配置文件的内容

# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
  mapper-locations: classpath:mybatis/**Mapper.xml
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
  level:
    com:
      example:
        demo: debug

将前端的东西导入static
项目——博客系统
项目——博客系统

实现common工具类

项目——博客系统
由于工具类后面我们基本都会用到,所以这里先实现一下

实现拦截器验证用户登录

  1. 步骤1创建一个Constant类,定义session的key
  2. 步骤2:创建一个普通类实现HandlerInterceptor接口,重写preHandle
  3. 步骤3:创建一个普通类实现 WebMvcConfigurer接口,重写addInterceptors
public class Constant {
    //登录信息存储到session中的key值
    public static final String SESSION_USERINFO_KEY = "session_userinfo_key";
}

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断登录业务
        HttpSession session = request.getSession(false);//会根据请求发送来的sessionId去服务器找对应的会话
        if(session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {//根据key拿到value
            return true;
        }
        response.setStatus(401);
        return false;
    }
}

@Configuration
public class AppConfig implements WebMvcConfigurer {
    //不拦截的url
    List<String> excludes = new ArrayList<>() {{
        add("/**/*.html");
        add("/js/**");
        add("/editor.md/**");
        add("/css/**");
        add("/img/**"); // 放行 static/img 下的所有文件
    }};

    @Autowired
    LoginInterceptor loginInterceptor;

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

实现统一数据返回格式

步骤1:创建一个普通的类,实现业务成功返回的方法和业务失败返回的方法
步骤2:创建一个普通的类实现ResponseAdvice接口,重写supports方法和beforeBodyWrite方法
代码

public class AjaxResult {

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

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

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

    /**
     * 业务执行失败返回的方法
     * @param code
     * @param msg
     * @return
     */
    public static HashMap<String,Object> fail(int code,String msg) {
        HashMap<String,Object> result = new HashMap<>();
        result.put("code",code);
        result.put("msg",msg);
        result.put("data","");
        return result;
    }
}
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) {
        if(body instanceof HashMap) {
            return body;
        }
        //如果body是字符串类型,需要特殊处理
        if(body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);
    }
}

实现加盐加密类

创建一个普通类,实现两个方法,encrypt方法和decrypt方法。

  • encrypt方法根据用户输入的密码,进行加盐加密,返回最终加密的密码
  • decrypt方法根据用户输入的密码和数据库存的加密的密码进行验证
public class SecurityUtil {

    /**
     * 对password进行加盐加密
     * @param password
     * @return
     */
    public static String encrypt(String password) {
        
    }

    /**
     * 验证password和数据库拿出来的finalPassword进行验证
     * @param password
     * @param finalPassword
     * @return
     */
    public static String decrypt(String password,String finalPassword) {
        
    }
}

实现encrypt方法

加盐思路:用UUID类生成一个32长度的字符串作为盐值,然后将盐值+password进行md5加密生成一个32长度的字符串,然后盐值+这个有md5加密的字符串,就是最终的结果

 /**
     * 对password进行加盐加密
     * @param password
     * @return
     */
    public static String encrypt(String password) {
        //每次生成内容不同,但是长度固定为32的字符串
        String salt = UUID.randomUUID().toString().replace("-","");
        //盐值+password进行md5加密
        String finalPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
        //返回盐值+密码,总共64位,存到数据库中
        return salt+finalPassword;
    }

实现decrypt方法

这个方法用来解密验证密码
思路:这个方法有两个参数password:待验证的密码,finalPassword:最终正确的密码(在数据库查询的密码),从finalPassword中提取出盐值,然后盐值+password进行md5加密生成一个字符串,然后盐值+字符串和finalPassword判断是否相等

/**
     * 验证password和数据库拿出来的finalPassword进行验证
     * password:待验证的密码
     * finalPassword:最终正确的密码(在数据库查询的密码)
     * @param password
     * @param finalPassword
     * @return
     */
    public static boolean decrypt(String password,String finalPassword) {
        //非空效验
        if(!StringUtils.hasLength(password) || !StringUtils.hasLength(finalPassword)) {
            return false;
        }
        if(finalPassword.length()!=64) {
            return false;
        }
        //提取出盐值
        String salt = finalPassword.substring(0,32);
        //使用盐值+密码生成一个32位的密码
        String securityPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
        //使用上一个32位的密码拼接上盐值进行密码验证
        return (salt+securityPassword).equals(finalPassword);
    }

实现SessionUtil类

这个工具类用来查询当前用户登录的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) {
            UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
            return userInfo;
        }
        return null;
    }
}

实现注册页面

这里开始就涉及到前后端交互了,要约定好前后端交互的接口

实现前端代码

那先来编写前端代码(打开reg.html进行编写代码)
记得要引入jquery
项目——博客系统
mysub方法

function mysub() {
            //1.非空效验
            var username = jQuery("#username");
            var password = jQuery("#password");
            var password2 = jQuery("#password2");
            if(username.val()=="") {
                alert("用户名为空");
                username.focus();
                return false;
            }
            if(password.val()=="") {
                alert("密码为空");
                password.focus();
                return false;
            }
            if(password2.val()=="") {
                alert("确认密码为空");
                password2.focus();
                return false;
            }
            if(password.val()!=password2.val()) {
                alert("两次输入的密码不一致");
                return false;
            }
            //2.发送ajax请求
            jQuery.ajax({
                url:"/user/reg",
                type:"POST",
                data:{
                    username:username.val(),
                    password:password.val()
                },
                success:function(result) {
                    if(result.code==200 && result.data==1) {
                        alert("注册成功");
                        if(confirm("是否去登录?")) {
                            location.href="login.html";
                        }
                    } else if(result.code==-2 && result.msg!=null) {
                        alert("注册失败,用户名已存在");
                    } else {
                        alert("注册失败请重试");
                    }
                }
            });
        }

实现后端代码

由前端代码可知,url为/user/reg,我们需要在AppConfig里放行这个url,因为默认是所有url都要拦截,但是注册不能拦截,因为还没注册怎么能登录呢
项目——博客系统
后端代码基本思路就是,controller调用service,service调用mapper
所以要在UserController中注入UserService,在UserService中注入UserMapper
项目——博客系统
项目——博客系统
在UserController中实现reg方法

@RequestMapping("/reg")
    public Object reg(String username,String password) {
        //1.非空效验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return AjaxResult.fail(-1,"非法参数");
        }
        //根据用户名查询用户
        UserInfo userInfo = userService.getUserByName(username);
        if(userInfo!=null && userInfo.getId()>0) {
            //用户名已经存在
            return AjaxResult.fail(-2,"用户名已存在");
        }
        //2.进行添加操作
        int result = userService.add(username, SecurityUtil.encrypt(password));
        if(result==1) {
            return AjaxResult.success(1,"添加成功");
        } else {
            return AjaxResult.fail(-1,"添加失败");
        }
    }

可以看到,我们还需要在UserService中实现getUserByName和add方法

public UserInfo getUserByName(String username) {
        return userMapper.getUserByName(username);
    }

    public int add(String username,String password) {
        return userMapper.add(username,password);
    }

实现完之后,还需要在UserMapper接口中定义getUserByName方法和add方法,然后在对应的xml文件中实现sql语句
UserMapper

 public UserInfo getUserByName(@Param("username") String username);

    public int add(@Param("username") String username,@Param("password") String password);

对应的UserMapper.xml文件
项目——博客系统

 <select id="getUserByName" resultType="com.example.demo.model.UserInfo">
        select * from userinfo where username=#{username}
    </select>

    <insert id="add">
        insert into userinfo(username,password)
        values(#{username},#{password})
    </insert>

到这里注册页面就实现完成了

实现登录页面

实现前端代码

打开login.html进行编写代码
还是一样,引入jquery,然后在submit设置onclick监听,然后实现mysub()方法
项目——博客系统

function mysub() {
            //1.非空效验
            var username = jQuery("#username");
            var password = jQuery("#password");
            if(username.val()=="") {
                alert("用户名为空");
                username.focus();
                return false;
            }
            if(password.val()=="") {
                alert("密码为空");
                password.focus();
                return false;
            }
            //2.发送ajax请求
            jQuery.ajax({
                url:"/user/login",
                type:"POST",
                data:{
                    "username":username.val(),
                    "password":password.val()
                },
                success:function(result) {
                    if(result.code==200 && result.data==1) {
                        location.href="myblog_list.html";
                    } else {
                        alert("用户名或密码错误");
                        username.focus();
                    }
                }
            });
        }

实现后端代码

先放行/user/login接口
然后在UserController中编写login方法

@RequestMapping("/login")
    public int login(HttpServletRequest request,String username,String password) {
        //1.非空效验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        //2.进行数据库查询操作
        UserInfo userInfo = userService.getUserByName(username);
        if(userInfo!=null && userInfo.getId()>0) {
            //用户名正确,验证密码
            if(SecurityUtil.decrypt(password,userInfo.getPassword())) {
                //将userInfo存到session中
                HttpSession session = request.getSession(true);
                session.setAttribute(Constant.SESSION_USERINFO_KEY,userInfo);
                return 1;
            }
        }
        
        return 0;
    }

实现个人主页

实现退出登录功能

当然由于退出登录功能在很多页面中都应该存在,所以后面可能就不详细说明

实现前端代码

退出登录功能后面很多页面都会用,所以我们新建一个Tool.js文件,将退出登录的前端方法写在里面
项目——博客系统
然后在Tool.js中编写代码

//退出登录
function onExit() {
    if(confirm("是否确认退出?")) {
        //发送ajax请求退出
        jQuery.ajax({
            url:"/user/logout",
            type:"POST",
            data:{},
            success: function(result) {
                location.href="login.html";
            },
            error:function(err) {
                if(err!=null && err.status==401) {
                    alert("用户未登录,即将跳转登录页面");
                    location.href="login.html";
                }
            }
        });
    }
}

编写myblog_list.html文件
并引入Tool.js和jquery
项目——博客系统
然后只需要修改一句代码即可
项目——博客系统
当你点击退出登录,它会自动调用Tool.js里的onExit()方法

实现后端代码

当用户点击退出登录,发送ajax请求到后端时,后端就将用户对应的session给删除即可。

在UserController中编写代码

@RequestMapping("/logout")
    public boolean logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session!=null && session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {
            session.removeAttribute(Constant.SESSION_USERINFO_KEY);
        }
        return true;
    }

初始化个人信息(包括个人文章列表的个人信息)和完成删除功能

当跳转到myblog_list.html时,就前端就调用initList()和myinfo()方法,这两个方法发送ajax请求去查询数据库,然后将个人信息和个人发表的文章返回给前端,前端再根据数据进行渲染

实现前端代码

<script>
        var descLen = 60;//简介最大长度
        //这个方法用来从正文中提取字符串
        function mySubStr(content) {
            if(content.length>desLen) {
                return content.substr(0,descLen);
            }
            return content;
        }
        //初始化个人列表信息
        function initList() {
            jQuery.ajax({
                url:"/art/mylist",
                type:"POST",
                data:{},//不用传uid,因为session中有userinfo,不能轻信前端传来的参数
                success:function(result) {
                    if(result.code==200 && result.data!=null && result.data.length>0) {
                        var html = "";
                        for(var i=0;i<result.data.length;i++) {
                            var item = result.data[i];
                            //如果state==2,说明是草稿箱里的文章,不显示出来
                            if(item.state==2) {
                                continue;
                            }
                            html+='<div class="blog">';
                            html+='<div class="title">'+item.title+'</div>';
                            html+='<div class="date">'+item.createtime+'</div>'+'<div class="desc">'+mySubstr(item.content)+' </div>';
                            html+='<div style="text-align: center;margin-top: 50px;">';
                            html+='<a href="blog_content.html?id='+item.id+'">查看详情</a>&nbsp;&nbsp;';
                            html+='<a href="blog_update.html?id='+item.id+'">修改</a>&nbsp;&nbsp;<a href="javascript:myDel('+item.id+')">删除</a></div>';
                            html+='</div>';
                        }
                        jQuery("#artlistDiv").html(html);
                    } else {
                        //此人没有发表文章
                        jQuery("#artlistDiv").html("<h1>暂无数据</h1>");
                    }
                },
                error:function(err) {
                    if(err!=null && err.statue==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
            });
        }
        initList();//当浏览器渲染引擎执行到此行时就会调用此方法

        //获取个人信息
        function myinfo() {
            jQuery.ajax({
                url:"user/myinfo",
                type:"POST",
                data:{},
                success:function(result) {
                    if(result.code==200 && result.data!=null) {
                        jQuery("#username").text(result.data.username);
                    }
                },
                error:function(err) {
                    if(err!=null && err.status==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
            });
        }
        myinfo();

        //删除功能
        function myDel(aid) {
            jQuery.ajax({
                url:"art/del",
                type:"POST",
                data:{
                    "aid":aid
                },
                success:function(result) {
                    if(result.code==200 && result.data==1) {
                        alert("删除成功");
                        location.href="myblog_list.html";
                    }
                },
                error:function(err) {
                    if(err!=null && err.status==401) {
                        alert("用户未登录");
                        location.href="login.html"

                    } else {
                        alert("删除失败");
                        location.href="myblog_list.html"
                    }
                    
                }
            });
        }

    </script>

代码很长,但其实主要也就三个主要的方法,这3个方法都有ajax请求,所以需要在后端进行处理,然后返回结果

实现后端代码

先处理initList的ajax请求
对于initList发送来的ajax请求,我们要根据sesion里存的userInfo的id去查文章表拿到该用户的所有文章信息,然后进行返回
同样,先进行注入
项目——博客系统

 @RequestMapping("/mylist")
    public List<ArticleInfo> myList(HttpServletRequest request) {
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null) {
            return articleService.getMyList(userInfo.getId());
        }
        return null;
    }

接下来一样要在ArticleService中实现getMyList方法
项目——博客系统
在ArticleService中也要先注入

 public List<ArticleInfo> getMyList(Integer uid) {
        return articleMapper.getMyList(uid);
    }

接下来在ArticleMapper中定义getMyList方法,然后在对应的xml文件中编写sql语句

public List<ArticleInfo> getMyList(@Param("uid") Integer uid);
<select id="getMyList" resultType="com.example.demo.model.ArticleInfo">
            select * from articleinfo where uid=#{uid}
     </select>

接下来处理myinfo的ajax请求

 @RequestMapping("/myinfo")
    public UserInfo myInfo(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session!=null && session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {
            UserInfo userInfo = SessionUtil.getLoginUser(request);
            return userInfo;
        }
        return null;
    }

接下来处理myDel发送来的ajax请求

@RequestMapping("/del")
    public Integer artDel(HttpServletRequest request,Integer aid) {
        //非空效验
        if(aid!=null && aid>0) {
            //根据文章id查询文章详情
            ArticleInfo articleInfo = articleService.getDetail(aid);
            //进行归属人验证
            UserInfo userInfo = SessionUtil.getLoginUser(request);
            if(articleInfo!=null && userInfo!=null && userInfo.getId()!=articleInfo.getUid()) {
                //归属人正确进行删除
                int result = articleService.artDel(aid);
                return result;
            }
        }
        return null;
    }

接下来得在ArticleService中实现getDetail方法和artDel方法

public ArticleInfo getDetail(Integer aid) {
        return articleMapper.getDetail(aid);
    }

    public Integer artDel(Integer aid) {
        return articleMapper.artDel(aid);
    }

接下来在ArticleMapper中定义getDetail方法和artDel方法,然后在对应的xml文件中编写sql语句

public ArticleInfo getDetail(@Param("aid") Integer aid);
 public Integer artDel(@Param("aid") Integer aid);
 <select id="getDetail" resultType="com.example.demo.model.ArticleInfo">
          select * from articleinfo where id=#{aid}
     </select>

  <delete id="artDel">
          delete from articleinfo where id=#{aid}
  </delete>

实现详情页(blog_content.html)

项目——博客系统
myblog_list.html的页面效果大概是这样的,其中删除功能已经实现了,那么现在来实现查看详情功能,当点击查看详情时,就会跳转blog_content.html页面,并将文章id传过去(如下图)
项目——博客系统

实现前端代码

那接下来打开blog_content.html来进行编写前端代码
这个页面同样也有退出登录功能,跟上面写法一样
项目——博客系统
在编写之前,我们需要在Tool.js中写一个方法getURLParam,这个方法用来从url上获取参数
这里直接给代码

// 获取当前 url 中某个参数的方法
function getURLParam(key){
    var params = location.search;
    if(params.indexOf("?")>=0){
        params = params.substring(1);
        var paramArr = params.split('&');
        for(var i=0;i<paramArr.length;i++){
            var namevalues = paramArr[i].split("=");
            if(namevalues[0]==key){
                return namevalues[1];
            }
        }
    }else{
        return "";
    }
}

然后开始编写blog_content.html

 //获取文章详细信息
            function getArtDetail() {
                //从url中获取文章id,也就是在myblog_list.html跳转到这里时url中的参数
                var aid = getURLParam("id");
                if(aid!=null && aid>0) {
                    //发送ajax请求,查询数据库拿到文章详情
                    jQuery.ajax({
                        url:"art/detail",
                        type:"POST",
                        data:{"aid":aid},
                        success:function(result) {
                            if(result.code==200 && result.data!=null) {
                                var art = result.data;
                                jQuery("#title").text(art.title);
                                jQuery("#date").text(art.createtime);
                                jQuery("#rcount").text(art.rcount);
                                editormd = editormd.markdownToHTML("editorDiv",{
                                    markdown : art.content
                                });
                                //根据uid获取个人信息
                                myInfo(art.uid);
                            }
                        }
                    });
                }
            }
            getArtDetail();

            //根据uid获取个人信息
            function myInfo(uid) {
                jQuery.ajax({
                    url:"user/myinfobyid",
                    type:"POST",
                    data:{"uid":uid},
                    success:function(result) {
                        if(result.code==200 && result.data!=null) {
                            jQuery("#username").text(result.data.username);
                        }
                    },
                    error:function(err) {
                        if(err!=null && err.status==401) {
                            alert("用户为登录,即将跳转登录页面");
                            location.href="login.html"
                        }
                    }
                });
            }

实现后端代码

上面前端代码也是两个ajax请求
先来处理getArtDetail方法发送来的ajax请求
要在AppConfig中放行detail接口
项目——博客系统

 @RequestMapping("/detail")
    public Object getDetail(Integer aid) {
        if(aid!=null && aid>0) {
            ArticleInfo articleInfo = articleService.getDetail(aid);
            //访问量加1
            synchronized (locker) {
                int result = articleService.rountAdd(aid,articleInfo.getContent()+1);
                if(result!=1) {
                    return AjaxResult.fail(-1,"访问量添加失败");
                }
            }
            //返回文章详情
            return AjaxResult.success(articleInfo);
        }
        return AjaxResult.fail(-1,"查询失败");
    }

接下来要在ArtclieService中实现rcountAdd方法

public int rcountAdd(Integer aid,Integer rcount) {
        return articleMapper.rcountAdd(aid,rcount);
    }

接下来在ArticleMapper中定义rcountAdd方法并在对应的xml文件中编写sql语句

 public int rcountAdd(@Param("aid") Integer aid,@Param("rcount") Integer rcount);
<update id="rcountAdd">
          update articleinfo set rcount=#{rcount} where id=#{aid}
     </update>

然后处理myinfo发送的ajax请求,要在AppConfig放行/user/myinfobyuid
项目——博客系统

@RequestMapping("/myinfobyuid")
    public UserInfo getMyInfoByUid(Integer uid) {
        if(uid!=null && uid>0) {
            return userService.getMyInfoByUid(uid);
        }
        return null;
    }
public UserInfo getMyInfoByUid(Integer uid) {
        return userMapper.getMyInfoByUid(uid);
    }
public UserInfo getMyInfoByUid(@Param("uid") Integer uid);
<select id="getMyInfoByUid" resultType="com.example.demo.model.UserInfo">
        select * from userinfo where id=#{uid}
    </select>

实现博客修改功能(blog_update.html)

项目——博客系统
当点击修改时,就会将跳转到blog_update.html并将id传过去

实现前端代码

同样这个页面有退出登录功能,实现方法跟上面一样
项目——博客系统

 function mysub(){
            // alert(editor.getValue()); // 获取值
            // editor.setValue("#123") // 设置值\
            var title = jQuery("#title");
            var content = editor.getValue();
            //非空效验
            if(title=="") {
                title.focus();
                alert("请先输入标题");
                return false;
            }
            if(content=="") {
                alert("请输入正文");
                return false;
            }
            jQuery.ajax({
                url:"/art/update",
                type:"POST",
                data:{
                    "aid":aid,
                    "title":title.val(),
                    "content":content
                },
                success:function(result) {
                    if(result.code==200 && result.data>0) {
                        alert("修改成功");
                        location.href="myblog_list.html";
                    } else {
                        alert("修改失败,请重试");
                    }
                },
                error:function(err) {
                    if(err!=null && err.status==401) {
                        alert("用户未登录");
                        location.href="login.html";
                        
                    }
                }
            });
        }

        //查询文章详情并展现
        function showArt() {
            //从url中获取文章id
            aid = getURLParam("id");
            if(aid!=null && aid>0) {
                //访问后端详情
                jQuery.ajax({
                    url:"/art/detailbyid",
                    type:"POST",
                    data:{"aid":aid},
                    success:function(result) {
                        if(result.code==200 && result.data!=null) {
                            var art = result.data;
                            jQuery("#title").val(art.title);
                            initEdit(art.content);
                        } else {
                            alert("您没有权限修改");
                            location.href="myblog_list.html";
                        }
                    },
                    error:function(err) {
                        if(err!=null && err.status==401) {
                            alert("用户还未登录,即将跳转登录页面");
                            location.href="login.html";
                        }
                    }
                });
            }
        }
        showArt();

实现后端代码

上面前端代码中涉及到两个ajax请求
先来处理showArt方法发送的ajax请求

 @RequestMapping("/detailbyid")
    public Object getDetailById(HttpServletRequest request,Integer aid) {
        if(aid!=null && aid>0) {
            //根据文章id查询文章详情
            ArticleInfo articleInfo = articleService.getDetail(aid);
            //文章归属人验证
            UserInfo userInfo = SessionUtil.getLoginUser(request);
            if(articleInfo!=null && userInfo!=null && articleInfo.getUid()==userInfo.getId()) {
                //文章归属人正确
                return AjaxResult.success(articleInfo);
            }
        }
        return AjaxResult.fail(-1,"查询失败");
    }

接下来处理mysub发送的ajax请求

@RequestMapping("/update")
    public int update(HttpServletRequest request,Integer aid,String title,String content) {
        //非空效验
        if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content) || aid == 0 || aid<=0) {
            return 0;
        }
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null && userInfo.getId()>0) {
            return articleService.update(aid,userInfo.getId(),title,content);
        }
        return 0;
    }

然后在ArticleService中实现update

 public int update(Integer aid,Integer uid,String title,String content) {
        return articleMapper.update(aid,uid,title,content);
    }

然后在ArticleMapper中定义update并在对应的xml文件中编写sql语句

public int update(@Param("aid") Integer aid,@Param("uid") Integer uid,
                      @Param("title") String title,@Param("content") String content);
<update id="update">
          update articleinfo set title=#{title},content=#{content}
          where id=#{aid} and uid=#{uid}
     </update>

实现博客编辑页

实现前端代码

 function mysub(){
            // alert(editor.getValue()); // 获取值
            // editor.setValue("#123") // 设置值
            var title = jQuery("#title");
            var content = editor.getValue();
            //非空效验
            if(title=="") {
                title.focus();
                alert("请先输入标题");
                return false;
            }
            if(content=="") {
                alert("请先输入正文");
                return false;
            }
            jQuery.ajax({
                url:"/art/release",
                type:"POST",
                data:{
                    "title":title.val(),
                    "content":content
                },
                success:function(result) {
                    if(result.code==200 && result.data>0) {
                        alert("发布成功");
                        location.href="myblog_list.html";
                    } else {
                        alert("发布失败,请重试");
                    }
                },
                error:function(err) {
                    if(err!=null && err.status==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
            });
        }

        //检查是否已经登录
        function checkIsLogged() {
            jQuery.ajax({
                url:"/user/islogged",
                type:"GET",
                success:function(result) {
                    return true;
                },
                error:function(err) {
                    alert("用户为登录,即将跳转登录页面");
                    location.href="login.html";
                }
            });
        }
        checkIsLogged();

        //保存文章到草稿箱,将state设置为2
        function draft() {
            var title = jQuery("#title");
            var content = editor.getValue();
            //非空效验
            if(title=="") {
                title.focus();
                alert("请先输入标题");
                return false;
            }
            if(content=="") {
                alert("请先输入正文");
                return false;
            }
            jQuery.ajax({
                url:"/art/draft",
                type:"POST",
                data:{
                    "title":title.val(),
                    "content":content
                },
                success:function(result) {
                    if(result.code==200 && result.data>0) {
                        alert("保存成功");
                        location.href="myblog_list.html";
                    } else {
                        alert("发布失败,请重试");
                    }
                },
                error:function(err) {
                    if(err!=null && err.status==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
            });
        }

实现后端代码

上面的前端代码总共涉及到3个ajax请求
先来处理 checkIsLogged方法的ajax请求,这个主要用来验证时候登录

 @RequestMapping("/islogged")
    public int isLogged(HttpServletRequest request, HttpServletResponse response) {
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null) {
            return 1;
        }
        response.setStatus(401);
        return 0;
    }

然后处理mysub()方法的ajax请求,这个方法用来发布文章,默认state1说明是发布的文章,state2是保存在草稿箱的文章

@RequestMapping("/release")
    public int releaseArt(HttpServletRequest request,String title,String content) {
        //非空效验
        if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
            return 0;
        }
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null && userInfo.getId()>0) {
            int result = articleService.releaseArt(title,content,userInfo.getId());
            if(result==1) {
                return 1;
            }
        }
        return 0;
    }

还要在ArtcileService中实现releaseArt方法,然后在ArticleMapper中定义releaseArt方法,并在对应的xml文件中编写sql语句

 @RequestMapping("/release")
    public int releaseArt(HttpServletRequest request,String title,String content) {
        //非空效验
        if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
            return 0;
        }
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null && userInfo.getId()>0) {
            int result = articleService.releaseArt(title,content,userInfo.getId());
            if(result==1) {
                return 1;
            }
        }
        return 0;
    }
 public int releaseArt(@Param("title")String title,@Param("content") String content,@Param("uid") Integer uid);
<insert id="releaseArt">
          insert into articleinfo (title,content,uid) values (#{title},#{content},#{uid})
     </insert>

最后处理保存文章也就是draft()的ajax请求,处理方法是将文章保存到数据库中,但是要将字段state设置为2,然后前端在渲染时,如果state==2,文章会显示在我的草稿箱中

 @RequestMapping("/draft")
    public int draftArt(HttpServletRequest request,String title,String content) {
        //非空效验
        if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
            return 0;
        }
        int state = 2;//将state设置为2
        UserInfo userInfo = SessionUtil.getLoginUser(request);
        if(userInfo!=null && userInfo.getId()>0) {
            //将文章保存到数据库,并将state设置为2,表示还未发布
            int result = articleService.draftArt(title,content,userInfo.getId(),2);
            return result;
        }
            return 0;
    }

还是老步骤在,ArticleService中实现draftArt,然后在ArticleMapper定义draftArt方法,并在对应的xml文件中编写sql语句

public int draftArt(String title,String content,Integer uid,Integer state) {
        return articleMapper.draftArt(title,content,uid,state);
    }
 public int draftArt(@Param("title") String title,@Param("content") String content,
                        @Param("uid") Integer uid,@Param("state") Integer state);
 <insert id="draftArt">
          insert into articleinfo (title,content,uid,state) values (#{title},#{content},#{uid},#{state})
     </insert>

实现我的草稿箱(draft_list.html)

项目——博客系统
当点击我的草稿箱时,就会跳转到draft_list.html页面

实现前端代码

draft_list.html的代码基本和myblog_list.html的代码一样,最主要的区别主要是state2还是state2

 var descLen = 60;//简介最大长度
    //字符串截取,将文章正文截取成简介
    function mySubstr(content) {
        if(content.length>descLen) {
            return content.substr(0,descLen);
        }
        return content;
    }
    //初始化个人列表信息
    function initList() {
        jQuery.ajax({
                url:"/art/mylist",
                type:"POST",
                data:{},//不用传uid,因为session中有userinfo,不能轻信前端的参数
                success:function(result) {
                   if(result.code==200 && result.data!=null && result.data.length>0) {
                    //todo:有文章
                    var html="";
                    var count = 0;
                    for(var i=0;i<result.data.length;i++) {
                        var item=result.data[i];
                        //如果state==2说明是草稿箱里的文章,显示出来
                        if(item.state==2) {
                            html+='<div class="blog">';
                            html+='<div class="title">'+item.title+'</div>';
                            html+='<div class="date">'+item.createtime+'</div>'+'<div class="desc">'+mySubstr(item.content)+' </div>';
                            html+='<div style="text-align: center;margin-top: 50px;">';
                            html+='<a href="javascript:publish('+item.id+')">发布文章</a>&nbsp;&nbsp;';
                            html+='<a href="blog_update.html?id='+item.id+'">修改</a>&nbsp;&nbsp;<a href="javascript:myDel('+item.id+')">删除</a></div>';
                            html+='</div>';
                            count++;
                        }
                    }
                    jQuery("#artlistDiv").html(html); 
                    if(count==0) {
                        //此人草稿箱没有文章
                        jQuery("#artlistDiv").html("<h1>暂无数据</h1>");
                    } 
                   } 
                },
                error:function(err) {
                    if(err!=null&&err.status==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
            });
    }
    initList();//当浏览器渲染引擎执行到此行时就会调用此方法
    //获取个人信息
    function myInfo() {
        jQuery.ajax({
                url:"/user/myinfo",
                type:"POST",
                data:{},
                success:function(result) {
                   if(result.code==200 && result.data!=null) {
                    jQuery("#username").text(result.data.username);
                   }
                },
                error:function(err) {
                    if(err!=null&&err.status==401) {
                        alert("用户未登录,即将跳转登录页面");
                        location.href="login.html";
                    }
                }
                
            });
    }
    myInfo();
    //删除功能
    function myDel(aid) {
        jQuery.ajax({
            url:"/art/del",
            type:"POST",
            data:{
                "aid":aid
            },
            success:function(result) {
                if(result.code==200 && result.data==1) {
                    alert("删除成功");
                    location.href="draft_list.html";
                    
                }
            },
            error:function(err) {
                if(err!=null&&err.status==401) {
                    alert("该用户没有删除权限!");
                } else {
                    alert("删除失败!");
                }
                location.href="draft_list.html";
            }
        });

    }

    //将草稿箱里的文章发布:将state设置成1
    function publish(aid) {
        jQuery.ajax({
            url:"/art/publish",
            type:"POST",
            data:{
                "aid":aid
            },
            success:function(result) {
                if(result.code==200 && result.data==1) {
                    alert("发布成功");
                    location.href="myblog_list.html";
                }
            },
            error:function(err) {
                if(err!=null&&err.status==401) {
                    alert("该用户没有发布权限!");
                } else {
                    alert("发布失败!");
                }
                location.href="draft_list.html";
            }
        });
    }
    

实现后端代码

上面涉及到的3个ajax请求,前两个ajax请求在之前的后端代码已经处理了,所以这里只需要再处理publish方法里的ajax请求即可

 @RequestMapping("/publish")
    public int publishArt(Integer aid) {
        if(aid!=null) {
            //将state设置为1
            int result = articleService.publishArt(aid,1);
            if(result==1) {
                return 1;
            }
        }
        return 0;
    }

然后在ArticleService中实现draftArt方法,接着在ArticleMapper中定义draftArt方法并在对应的xml文件中编写sql语句

public int publishArt(Integer aid,Integer state) {
        return articleMapper.publishArt(aid,state);
    }
 public int publishArt(@Param("aid") Integer aid,@Param("state") Integer state);
<update id="publishArt">
          update articleinfo set state=#{state} where id=#{aid}
     </update>

实现博客主页(blog_list.html)

项目——博客系统
博客主页存放所有的博客,有5个按钮分别是,查看全文,首页,上一页,下一页和末页

首页,上一页,下一页和末页就属于分页功能了,接下来讲一下分页的思路
分页要素:

  1. 页码(pageIndex):要查询第几页的数据
  2. 每页显示多少条数据(pageSize):每页展示最大长度数据
    首先所有的数据都是从数据库中去查询的,查询的语句中有一个关键字limit,这样就可以限制发送多少条记录去前端,比如limit pageSize ,就查询pageSize条记录,然后发送去前端然后进行渲染页面
    数据库还有一个关键字就是offset(偏移量),比如说limit 2 offer 2,就是跳过前两条记录,然后查询第3,第4条记录。
    那么比如你想查询第pageIndex页的数据,那么它的偏移量就是pageSize*(pageIndex-1)
    所以分页公式(偏移量):pageSize*(pageIndex-1)
    分页语法:select * from articleinfo limit pageSize offset pageSize * (pageIndex-1)

实现前端代码

项目——博客系统

 var descLen = 200;//简介最大长度
    //字符串截取,将文章正文截取成简介
    function mySubstr(content) {
        if(content.length>descLen) {
            return content.substr(0,descLen);
        }
        return content;
    }
    var pindex = 1;//当前的页码
    var psize = 2;//每页显示的条数信息
    var totalpage = 1;//总页数

    //初始化分页参数,尝试从url中获取pindex和psize
    function initPageParam() {
        var pi = getURLParam("pindex");
        if(pi!="") {
            pindex=pi;
        }
        var ps = getURLParam("psize");
        if(ps!="") {
            psize=ps;
        }
    }
    initPageParam();

    //查询分页数据
    function getList() {
        jQuery.ajax({
            url:"/art/list",
            type:"GET",
            data:{
                "pindex":pindex,
                "psize":psize
            },
            success:function(result) {
                if(result.code==200 && result.data!=null && result.data.length>0) {
                    //循环拼接控制document
                    var finalHtml = "";
                    for(var i=0;i<result.data.length;i++) {
                        var item = result.data[i];
                        //如果state==2说明是草稿箱的文章,不显示出来
                        if(item.state==2) {
                            continue;
                        }
                        finalHtml+='<div class="blog">';
                        finalHtml+='<div class="title">'+item.title+'</div>';
                        finalHtml+='<div class="date">'+item.createtime+'</div>';
                        finalHtml+='<div class="desc">'+mySubstr(item.content)+'</div>';
                        finalHtml+='<a href="blog_content.html?id='+item.id+'" class="detail">查看全文</a>';
                        finalHtml+='</div>';
                    }
                    jQuery("#listDiv").html(finalHtml);
                }
            }
        });
    }
    getList();

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

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

    //上一页
    function beforeClick() {
        if(pindex<=1) {
            alert("前面已经没有内容了!");
            return false;
        }
        pindex-=1;
        location.href="blog_list.html?pindex="+pindex+"&psize="+psize;
    }

    //下一页
    function nextClick() {
        pindex = parseInt(pindex);
        if(pindex>=totalpage) {
            //已经是最后一页
            alert("后面已经没有内容了哦!");
            return false;
        }
        pindex+=1;
        location.href="blog_list.html?pindex="+pindex+"&psize="+psize;
    }

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

实现后端代码

上面涉及到两个ajax请求
两个请求的url都需要放行
项目——博客系统

先来处理getTotalPage方法的ajax请求

@RequestMapping("/totalpage")
    public Integer getTotalCount(Integer psize) {
        if(psize!=null) {
            //参数有效
            int totalCount = articleService.getTotalCount();
            int totalPage = (int)Math.ceil(totalCount*1.0/psize);
            return totalPage;
        }
        return null;
    }

接下来在ArticleService中实现getTotalCount方法,然后在ArticleMapper中定义getTotalCount方法并在对应的xml文件中编写sql语句

public int getTotalCount() {
        return articleMapper.getTotalCount();
    }
public int getTotalCount();
<select id="getTotalCount" resultType="java.lang.Integer">
          select count(*) from articleinfo where state=1
     </select>

接下来处理getList的ajax请求

public List<ArticleInfo> getList(Integer psize,Integer offset) {
        return articleMapper.getList(psize,offset);
    }

然后在ArticleService中实现getList,还要在ArticleMapper中定义getList并在对应的xml文件中编写sql语句

 public List<ArticleInfo> getList(@Param("psize") Integer psize,@Param("offset") Integer offset);
 <select id="getList" resultType="com.example.demo.model.ArticleInfo">
          select * from articleinfo limit #{psize} offset #{offset}
     </select>

将session持久化到redis

首先你需要在你的linux环境上安装redis,这里就不介绍怎么安装了
然后在你的项目上的application.properties上配置连接redis即可,springboot已经内置了将session持久化到redis,所以你只需要在application.properties上配置即可

spring.redis.host=#你redis安装在哪台机器的ip地址
spring.redis.password=#redis的密码
spring.redis.port=6379#redis默认端口号
spring.redis.database=0#redis操作的数据库
spring.session.store-type=redis
server.servlet.session.timeout=1800#session存到redis的过期时间
spring.session.redis.flush-mode=on_save#会将session保存到redis本地
spring.session.redis.namespace=spring:session

其他扩展功能

  • 定时发布功能
  • 更换头像
  • 冻结账号
    等等,大家可以尝试自己实现,下面这个源代码链接,后续有新功能也会将源代码更新

源码链接:https://gitee.com/maze-white/project/tree/master/blog_system_ssm