SSM版本个人博客系统实现
SSM版本的个人博客系统
文章目录
- SSM版本的个人博客系统
-
- 统一的数据返回处理
- 关于前端的一些问题
- 实现注册功能
- 实现登录的功能
- 存储session
- 获取用户的信息
-
- 获取左侧的个人信息
- 获取右侧的博客列表
-
- 时间格式化
- 删除操作
- 注销功能(退出登录)
- 查看文章的详情页
-
- 排查问题
- 实现阅读量累计
- 新增文章
-
- 小优化
- 修改文章
- 加盐算法
- 实现文章的分页功能
- Session持久化
在正式写后端程序之前,我已经将博客系统的前端页面写好,详情可以见我的gitee
项目源码
实现步骤:
-
创建一个SSM项目
-
准备项目
a.删除项目中用不到的文件
b.在resources下面的static包下引入起前端页面
c.添加数据库中常用的配置(properties文件)
spring.datasource.url=jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password= 1111 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #在resources下面建一个"mapper"的文件夹,里面放的就是xml文件,就使用下面的路径 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl logging.level.com.example.demo=debug
-
使用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 unique,password varchar(100) not null ,photo varchar(500) default '',createtime datetime,updatetime datetime,`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,updatetime datetime,uid int not null,rcount int not null default 1,`state` int default 1 )default charset 'utf8mb4';-- 创建视频表 drop table if exists videoinfo; create table videoinfo(vid int primary key,`title` varchar(250),`url` varchar(1000),createtime datetime,updatetime datetime,uid int )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)values('Java','Java正文',1);-- 添加视频 insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);--
-
创建出合适的分层
统一的数据返回处理
对于一个项目来说,统一的数据返回是前后端交互很重要
AjaxResult类:
package com.example.demo.common;import lombok.Data;import java.io.Serializable;/*统一的数据格式返回最终以JSON的形式来返回*/
@Data
public class AjaxResult implements Serializable {//实现Serializable序列化//状态码private Integer code;//状态码描述信息private String msg;//返回的数据(不知道具体是什么类型,所以采用Object)private Object data;/*操作成功的结果*/public static AjaxResult success(Object data) {AjaxResult ajaxResult = new AjaxResult();ajaxResult.setCode(200);ajaxResult.setMsg("");//成功了就不返回信息了ajaxResult.setData(data);return ajaxResult;}//像上面写,数据是定死的,所以可以使用方法的重载来实现,一共就是3中重载的方法public static AjaxResult success(Integer code,Object data) {AjaxResult ajaxResult = new AjaxResult();ajaxResult.setCode(code);ajaxResult.setMsg("");//成功了就不返回信息了ajaxResult.setData(data);return ajaxResult;}public static AjaxResult success(Integer code,String msg,Object data) {AjaxResult ajaxResult = new AjaxResult();ajaxResult.setCode(code);ajaxResult.setMsg("msg");//成功了就不返回信息了ajaxResult.setData(data);return ajaxResult;}/* 返回失败的结果,也是重载*/public static AjaxResult fail(Integer code,String msg) {AjaxResult ajaxResult = new AjaxResult();ajaxResult.setCode(code);ajaxResult.setMsg("msg");ajaxResult.setData(null);return ajaxResult;}public static AjaxResult fail(Integer code,String msg,Object data) {AjaxResult ajaxResult = new AjaxResult();ajaxResult.setCode(code);ajaxResult.setMsg("msg");ajaxResult.setData(data);return ajaxResult;}}
ResponseAdvice类:
package com.example.demo.config;import com.example.demo.common.AjaxResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import javax.annotation.Resource;/* 这个类就是一个保底的类,要是忘记调用AjaxResult类,就只能依靠这个类在返回之前确保返回的是JSON格式的数据*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {//Jackson对象注入,用于将String类型转换成JSON@Resourceprivate ObjectMapper objectMapper;//这个方法就是一个开关,返回的是true时,才会执行beforeBodyWrite方法@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows //异常@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//body是Object格式的,所以要先检验一下数据类型if (body instanceof AjaxResult) {//说明body已经是AjaxResult的JSON类型了,所以没事return body;}if (body instanceof String) {//要是body是String,要想转换成JSON格式,就要用到Jackson来转换return objectMapper.writeValueAsString(AjaxResult.success(body));}//body是正常的数据类型return AjaxResult.success(body);}
}
关于前端的一些问题
有一个注意点:在修改前端代码的时候,前端页面可以没有生效,此时极大的概率是缓存问题
几种解决方案:
- 首先可以试试重启一下IDEA中的项目
- 不行的话,就删除目录下的target文件夹,之后再重启IDEA来重新生成target文件夹
- 使用强制刷新来刷新浏览器(CTRL + F5),之后打开F12看看源代码有没有改变
- 要是还是不行的话,就尝试在url后面添加一个?参数,来让浏览器重新加载
在进行前后端的交互的时候,使用的基本上都是jQuery的ajax,所以对于ajax要很熟悉
jQuery.ajax({url:"",//请求的地址type:"",//请求类型是GET/POST....data:{},//请求的参数,也就是要传递给后端的参数success: function(){//用于接收后端的返回值//........}
})
一个实现注册请求的ajax示例:
jQuery.ajax({url:"/user/reg",type:"POST",data:{"username":username.val(),"password":password.val()},success:function(result){//这里的data是受影响的行数if(result != null && result.code == 200 && result.data == 1){//后端返回响应且成功了if(confirm("恭喜您,注册成功是否要跳转到登录页面?")){location.href = "/login.html";}}else{alert("抱歉,注册失败,请稍后再试");}}})
实现注册功能
在后端实现注册功能的时候,其实本质上就是向数据库中的userinfo表中添加一行
逻辑调用关系:由于存在controller调用service,service调用mapper接口,所以可以先实现mapper接口,之后再向上传递,会比较好
注意:在创建出一个类的时候,应该首先考虑要不要加上注解,应该加上什么注解
创建出一个userinfo的实体类
package com.example.demo.entity;import lombok.Data;import java.time.LocalDateTime;
//使用@Data省的写很多的getter setter toString hashcode方法
@Data
public class Userinfo {//使用Integer比int更好,因为Integer的兼容性更好,传null时,int接收会报错,Integer接收不会报错private Integer id;private String username;private String password;private String photo;private LocalDateTime createtime;private LocalDateTime updatetime;private Integer state;
}
UserMapper:
package com.example.demo.mapper;import com.example.demo.entity.Userinfo;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper {int reg(Userinfo userinfo);
}
UserMapper.xml :
有一个很重要的点:在xml中写SQL语句的时候,只有select语句要写resultType,其他的语句都不用写返回值
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--命名空间要根据目录结构依次写到接口-->
<mapper namespace="com.example.demo.mapper.UserMapper"><insert id="reg">insert into userinfo(username,password)values (#{username},#{password})</insert></mapper><!--这个xml的名字要和上面的mapper包里面的接口名是一致的,这样子就能建立映射关系-->
UserService:
package com.example.demo.service;import com.example.demo.entity.Userinfo;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class UserService {@Resourceprivate UserMapper userMapper;public int reg(Userinfo userinfo) {return userMapper.reg(userinfo);}
}
UserController :
package com.example.demo.controller;import com.example.demo.common.AjaxResult;
import com.example.demo.entity.Userinfo;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.apache.coyote.http11.upgrade.UpgradeInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user") //使用@RequestMapping可以接收GET和POST请求
//RequestMapping是要根据前端在ajax中的url中的参数来确定的,所以前端的ajax很重要
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/reg")public AjaxResult reg(Userinfo userinfo) {//非空判断//虽然前端已经进行了非空检查,但是用户可能会通过别的方式直接访问url绕过前端的非空校验,所以作为后端,应该要考虑到这一点//所以在后端也是要写非空校验的if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||!StringUtils.hasLength(userinfo.getPassword())){return AjaxResult.fail(-1,"非法参数");}//不是空的话,就直接返回成功的响应就行了//这里响应的是1,所以前端在进行成功判断的时候才有result.data == 1 这一条return AjaxResult.success(userService.reg(userinfo));}}
以上就是所有的注册功能的实现,点击“确定”就会跳转到登录的页面
实现登录的功能
前端中的部分代码(重点是ajax的前后端交互)
<script>function mysub(){//1.非空校验var username = jQuery("#username");var password = jQuery("#password");if(username.val() == ""){alert("请先输入账号");username.focus();return;}if(password.val() == ""){alert("请先输入密码");password.focus();return;}//2.ajax请求登录接口jQuery.ajax({url:"/user/login",tpye:"POST",data:{"username":username.val(),"password":password.val()},success: function(result){//xiugaide if(result != null && result.code == 200 && result.data == 1){//登录成功location.href = "myblog_list.html";}else{alert("用户名或者密码错误,请重新输入");}}})}</script>
业务上的登录在数据库层面就是查询
这里有一个问题:要保证数据库中没有相同的用户名,可是在创建数据库的时候并没有考虑用户名的唯一性(unique),所以只能现在改一下
alter table 表名 add unqiue(列名)
alter table userinfo add unique(username);
UserMapper :
//登录
//这里只传入一个用户名,在后面的controller中进行密码判断就行了,主要是为了保护密码的安全,所以只传一个用户名
Userinfo getUserByName(@Param("username") String username);
UserMapper.xml:
<!--登录--><select id="getUserByName" resultType="com.example.demo.entity.Userinfo">select * from userinfo where username = #{username};</select>
UserService:
public Userinfo getUserByName(String username) {return userMapper.getUserByName(username);
}
UserController:
//登录操作
@RequestMapping("/login")
public AjaxResult login(String username,String password) {//1.进行非空判断if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){//说明没有传入任何参数return AjaxResult.fail(-1,"非法请求");}//2.查询数据库Userinfo userinfo = userService.getUserByName(username);if (userinfo != null && userinfo.getId() > 0) {//能获得id就说明用户名一定是在数据库中,说明是有效用户if (password.equals(userinfo.getPassword())){//要是密码正确,在将数据返回之前,考虑到隐私,隐藏密码userinfo.setPassword("");return AjaxResult.success(userinfo);}}return AjaxResult.fail(0,null);
}
最后的效果就是输入正确的账号密码登录,然后跳转到博客详情页
存储session
首先要知道为什么要存储session?
session就是会话的意思,一般都是用在服务端记录用户信息,可以用来标识当前的用户
session是键值对的形式,可以定义一个全局变量作为key
ApplicationVariable 类:
package com.example.demo.common;//关于全局变量的类
public class ApplicationVariable {//用户的session的key值public static final String USER_SESSION_KEY = "USER_SESSION_KEY";
}
在登录操作的时候就要创建session
UserController:
//登录操作
@RequestMapping("/login")
public AjaxResult login(HttpServletRequest request, String username, String password) {//1.进行非空判断if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){//说明没有传入任何参数return AjaxResult.fail(-1,"非法请求");}//2.查询数据库Userinfo userinfo = userService.getUserByName(username);if (userinfo != null && userinfo.getId() > 0) {//能获得id就说明用户名一定是在数据库中,说明是有效用户if (password.equals(userinfo.getPassword())){//将用户的session存储下来//参数为true:要是没有session就创建一个会话HttpSession session = request.getSession(true);//设置session的key和valuesession.setAttribute(ApplicationVariable.USER_SESSION_KEY,userinfo);//要是密码正确,在将数据返回之前,考虑到隐私,隐藏密码userinfo.setPassword("");return AjaxResult.success(userinfo);}}return AjaxResult.fail(0,null);
}
此时要考虑一下:是不是所有的页面都能给未登录的用户看呢?
之前的方法是每次进入一个页面之前都要做很多的session判断,来判断能不能进入当前的页面,但是现在可以实现一个拦截器,统一地进行拦截,有效地解决了代码的重复
哪些页面是不能拦截的–>不登录的用户也是可以看的?
- 注册页面 && 接口
- 登录页面 && 接口
- 博客列表页
- 博客详情页
blog_list是展示所有用户写的博客,不拦截
myblog_list是展示登录用户写的博客,要拦截(登录之后才能看到)
像博客编辑页、myblog_list 肯定是要先登录的,所以要对未登录的用户进行拦截
实现拦截器就是两个步骤:
- 实现一个普通的拦截器
- 设置拦截规则
LoginInterceptor :
package com.example.demo.config;import com.example.demo.common.ApplicationVariable;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;//1.实现一个普通的拦截器
//a.实现HandlerInterceptor
//b.重写preHandle,要是返回值时true,说明可以继续流程,返回值是false说明被拦截了,不能继续了
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//登录拦截器本来就是拦截没有登录的用户,所以没有session会话也不会创建HttpSession session = request.getSession(false);if (session != null && session.getAttribute(ApplicationVariable.USER_SESSION_KEY) != null) {//说明存在session且session的值不为空return true;}//要是不存在session,就直接跳转到登录页面response.sendRedirect("/login.html");return false;}
}
AppConfig:
package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;//2.设置拦截规则
@Configuration
public class AppConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/")//下面都是不要拦截的.excludePathPatterns("/css/").excludePathPatterns("/editor.md/").excludePathPatterns("/img/").excludePathPatterns("/js/").excludePathPatterns("/login.html").excludePathPatterns("/reg.html").excludePathPatterns("/blog_list.html").excludePathPatterns("/blog_content.html").excludePathPatterns("/art/detail").excludePathPatterns("/art/incr-rcount").excludePathPatterns("/user/getuserbyid").excludePathPatterns("/art/listbypage").excludePathPatterns("/user/login").excludePathPatterns("/user/reg");}
}
以后都只要输入一遍用户名 + 密码就能自由访问了,要是没有登录,有些页面就不能访问并且会跳转到登录页面
获取用户的信息
在登录之后,展示博客列表时,需要前端给后端发送请求,获取用户的信息(用户名、文章数量、写的博客)
信息可以分为左右两侧的信息
获取左侧的个人信息
首先是获取左侧的信息:主要是登录的用户名 和 写的博客数量
前端的主要代码:
<script>function showInfo(){jQuery.ajax({url:"/user/showinfo",type:"POST",data: "", //前端请求的时候并不需要任何参数success : function (result){if(result != null && result.code == 200){//说明后端已经返回了个人信息jQuery("#username").text(result.data.username);//artTotal在原本数据库中是没有的,所以后端要新建一个artTotal属性jQuery("#artTotal").text(result.data.artTotal);}else{alert("个人信息加载失败,请刷新重新尝试!");}}})}//执行方法showInfo();</script>
由于原本的Userinfo并没有artTotal属性,所以创建出一个新的实体类,继承了Userinfo
package com.example.demo.entity.vo;import com.example.demo.entity.Userinfo;
import lombok.Data;import java.net.InetAddress;@Data
public class UserinfoVO extends Userinfo {public Integer artTotal;//用户发表的博客总数
}
由于要拿到博客的数量,所以要查询 articleinfo 表,查询一个表,就要创建出一个对应的实体类、Mapper、Service、Controller
ArticleMapper:
package com.example.demo.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface ArticleMapper {//根据用户的id来查询博客数量int getArtTotalByUid(@Param("uid") int uid);
}
对应的ArticleMapper.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--命名空间要根据目录结构依次写到接口-->
<mapper namespace="com.example.demo.mapper.ArticleMapper"><select id="getArtTotalByUid" resultType="Integer">
-- 注意:uid代表作者的身份,id表示博客的数量,注意这里要使用count(*)计算出行数select count(*) from articleinfo where uid = #{uid};</select></mapper><!--这个xml的名字要和上面的mapper包里面的接口名是一致的,这样子就能建立映射关系-->
此时可以自动生成一个单元测试,来测试一下这个接口到底是不是正确的,免得后面写完了Service和Controller出错,更重要的是可以增加自己编码的信心
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dFmMngUJ-1681609448499)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20230408210107671.png)]
在写完了mapper之后,就可以写service和controller了
ArticleService:
package com.example.demo.service;import com.example.demo.mapper.ArticleMapper;
import org.apache.coyote.http11.upgrade.UpgradeInfo;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class ArticleService {@Autowiredprivate ArticleMapper articleMapper;public Integer getArtTotalByUid(Integer uid){return articleMapper.getArtTotalByUid(uid);}
}
要获得用户的博客数量,就要先获得session会话,之后才能进行操作,获取session这个动作在后面也会进程用到,所以将它变成一个common包下面的类中的一个方法应该是更好的选择
common包下面的UserSessionUtils:
package com.example.demo.common;import com.example.demo.entity.Userinfo;
import org.apache.coyote.http11.upgrade.UpgradeInfo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;//这个类是用来存放登录用户的session操作的
public class UserSessionUtils {//获取登录用户的sessionpublic static Userinfo getUser(HttpServletRequest request){HttpSession session = request.getSession();if (session != null && session.getAttribute(ApplicationVariable.USER_SESSION_KEY) != null) {return (Userinfo) session.getAttribute(ApplicationVariable.USER_SESSION_KEY);}return null;}}
开始写UserController:(首先要在前面将articleService对象注入)
@Autowired
private ArticleService articleService;@RequestMapping("/showinfo")//这个路径是根据前端ajax中的url规定好的
public AjaxResult showInfo(HttpServletRequest request){//定义包含artTotal属性的对象UserinfoVO userinfoVO = new UserinfoVO();//调用common包中的获取session方法Userinfo userinfo = UserSessionUtils.getUser(request);if (userinfo == null) {AjaxResult.fail(-1,"非法请求");}//Spring提供的深拷贝的方式,将userinfo深拷贝给userinfoVOBeanUtils.copyProperties(userinfo,userinfoVO);//通过userinfo的id来查找userinfoVO的博客数量userinfoVO.setArtTotal(articleService.getArtTotalByUid(userinfo.getId()));//前端最后要的是username和artTotal,这两个属性都在userinfoVO对象中,所以直接返回就行了return AjaxResult.success(userinfoVO);
}
这样子每次刷新就会去读取数据库,并且在左侧显示用户名和博客数量
获取右侧的博客列表
查询不同的表就要有对应的实体类、Mapper、Service、Controller
首先要创建出一个ArticleInto实体类,将数据库中的字段进行对应的实体化
package com.example.demo.entity;import lombok.Data;import java.time.LocalDateTime;//创建出一个文章的实体类
@Data
public class ArticleInfo {private Integer id;private String title;private String content;private LocalDateTime createtime;private LocalDateTime updatetime;private String uid;private String rcount;private String state;
}
在ArticleMapper中:
//根据uid获取用户的博客列表(uid指的是用户的id,数据库中的id指的是文章对用的id)
//返回的是ArticleInfo类型的list
List<ArticleInfo> getMyList(@Param("uid") int uid);
ArticleMapper.xml :
<!--返回的是ArticleInfo实体类--><select id="getMyList" resultType="com.example.demo.entity.ArticleInfo">select * from articleinfo where uid = #{uid};</select>
ArticleService:
@Autowiredprivate ArticleMapper articleMapper;
public List<ArticleInfo> getMyList(Integer uid){return articleMapper.getMyList(uid);
}
ArticleController:
package com.example.demo.controller;import com.example.demo.common.AjaxResult;
import com.example.demo.common.UserSessionUtils;
import com.example.demo.entity.ArticleInfo;
import com.example.demo.entity.UserInfo;
import com.example.demo.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.List;@RestController
//所有的@RequestMapping后面的参数都是在前端代码中指定的
@RequestMapping("/art")
public class ArticleController {@Autowiredprivate ArticleService articleService;@RequestMapping("/getlist")public AjaxResult getMyList(HttpServletRequest request) {//首先先获取到对应的用户信息UserInfo userInfo = UserSessionUtils.getUser(request);if (userInfo == null) {//说明当前不存在session,也就是没有登录(虽然存在拦截器,但是还是要进行判断一下是否是真的登录了)return AjaxResult.fail(-1, "非法请求");}//说明已经正常登录了List<ArticleInfo> list = articleService.getMyList(userInfo.getId());return AjaxResult.success(list);//问题:在xml中传入的是uid,为什么这里传入的是userInfo.getId()????//这里涉及到了userinfo和articleinfo两张表的关系了//在articleinfo表中id表示每篇文章对应的id,uid表示写文章的用户//在userinfo中id表示当前登录用户的信息,所以userinfo中id对应着articleinfo表中uid,所以这里使用的是userInfo.getId()}
}
但是,时间的格式并不是很对,所以需要进行时间的格式化调整
时间格式化
-
使用全局配置方法
在properties文件中,使用
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone= GMT +8
这个GMT默认是格林尼治时间,由于我们是在东八区,所以时间要加8
但是这种全局配置的方法只能针对Date类型,对LocalDateTime类型是不生效的
- 由于上面的全局配置方式对LocalDateTime没有效果,所以可以使用@JsonFormat注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT +8")private LocalDateTime createtime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT +8")private LocalDateTime updatetime;
对每个时间变量单独进行时间的格式化
时间格式化后显示效果:
删除操作
前端的部分代码:
//删除方法function myDel(id){if(confirm("确定要删除文章吗?")){jQuery.ajax({url:"art/del",type:"POST",data:{"id":id},//将要删除的文章的id传给后端success: function(result){if(result != null && result.code == 200 && result.data == 1){alert("删除成功!");//删除之后,刷新页面location.href = location.href;}else{alert("抱歉,删除失败,请重试!");}}})}}
ArticleMapper:
//删除文章
//在删除文章的时候必须要验证当前登录的用户是不是文章的作者
//在articleinfo中的id表示文章的id,uid表示写这篇文章的用户的id,在controller中只要判断一下uid与当前登录的用户的id是不是一样就也可以了
int del(@Param("id") Integer id,@Param("uid") Integer uid);
ArticleMapper.xml :
<delete id="del">delete from articleinfo where id = #{id} and uid = #{uid};
</delete>
ArticleService:
public Integer del(Integer id,Integer uid){return articleMapper.del(id,uid);
}
ArticleController:
@RequestMapping("/del")
//前端传过来的id是文章的id
public AjaxResult del(HttpServletRequest request, Integer id) {if (id == null || id <= 0) {//根本就不存在这个文章AjaxResult.fail(-1,"非法请求");}UserInfo userInfo = UserSessionUtils.getUser(request);if(userInfo == null){AjaxResult.fail(-2,"用户未登录");}//userinfo中的id就是articleinfo中的uid,都表示用户的idreturn AjaxResult.success(articleService.del(id,userInfo.getId()));
}
注意:
return AjaxResult.success(articleService.del(id,userInfo.getId()));
并不一定能删除文章
登录uid为2的用户去删除文章id为1时,最后并没有删除,
对应到前端中的
result。data = 0,所以最后还是没有删除,满足了业务的需求。
注销功能(退出登录)
前端部分代码:
<a href="javascript:loginout()">注销</a>
//实现注销方法function logout(){if(confirm("确认退出吗?")){jQuery.ajax({url:"/user/logout",type:"POST",data:{},success: function(result){if(result != null && result.code == 200){ //返回到登录页面location.href = "/login.html";}}})}}
退出登录不需要查询数据库,所以直接在controller写就行了
//实行注销功能@RequestMapping("/logout")public AjaxResult logout(HttpSession session){//将session中key的值删除了,那session也就没了session.removeAttribute(ApplicationVariable.USER_SESSION_KEY);return AjaxResult.success(1);}
查看文章的详情页
- 从url中得到文章 id的值(前端代码中体现)
- 从后端articleinfo表查询出当前文章的详细信息(已经uid)
- 根据查询到的uid来查询用户的信息
- 实现阅读量+1
前端的部分代码:
function getArtDetail(id){if(id == ""){alert("非法参数");return;}jQuery.ajax({url:"/art/detail",type:"POST",data:{"id":id},success: function(result){if(result != null && result.code == 200){//说明请求成功了jQuery("#title").html(result.data.title);jQuery("#updatetime").html(result.data.updatetime);jQuery("#rcount").html(result.data.rcount);initEdit(result.data.content);showUser(result.data.uid);}else{alert("请求失败,请重试!");}}});}
从后端中查询当前文章的信息:
ArticleMapper:
//获取文章的详情
ArticleInfo getDetail(@Param("id") Integer id);
ArticleService:
public ArticleInfo getDetail(Integer id){return articleMapper.getDetail(id);
}
ArticleController:
@RequestMapping("/detail")
public AjaxResult getDetail(Integer id){if(id == null || id <= 0){return AjaxResult.fail(-1,"非法请求");}return AjaxResult.success(articleService.getDetail(id));
}
左侧的作者信息要去数据库中拿:
前端中的部分代码:
//查询用户的详细信息function showUser(id){jQuery.ajax({url:"/user/getuserbyid",type:"POST",data:{"id":id},success:function(result){if(result != null && result.code ==200 && result.data.id > 0){jQuery("#username").text(result.data.username),jQuery("#artCount").text(result.data.artCount)}else{alert("抱歉,查询用户信息失败,请重试!");}}})}
UserMapper:
//在文章详情中获取左侧的作者信息
UserInfo getUserById(@Param("id") Integer id);
UserMapper.xml :
<select id="getUserById" resultType="com.example.demo.entity.UserInfo">select * from userinfo where id = #{id};
</select>
UserService:
public UserInfo getUserById(Integer id){return userMapper.getUserById(id);
}
UserController :
//在文章详情页获取左侧的博客作者的信息
//url中一定不要使用大写字母,windows对于大小写不敏感,但是Linux对大小写敏感,所以可能会出现错误
@RequestMapping("getuserbyid")
public AjaxResult getUserById(Integer id){if(id == null || id <= 0){return AjaxResult.fail(-1,"非法参数");}UserInfo userInfo = userService.getUserById(id);if(userInfo == null || userInfo.getId() <= 0){//用户不存在return AjaxResult.fail(-2,"非法参数");}//去除敏感的信息userInfo.setPassword("");UserInfoVO userInfoVO = new UserInfoVO();//深拷贝,将userInfo拷贝给userInfoVOBeanUtils.copyProperties(userInfo,userInfoVO);//查询当前用户发布过多少篇文章userInfoVO.setArtTotal(articleService.getArtTotalByUid(id));return AjaxResult.success(userInfoVO);
}
之所以要引入userInfoVO,是因为userInfoVO中有artTotal,可以统计文章数
在启动项目之后,却发现了错误
排查问题
遇到错误其实是很常见的,最重要的就是找到哪里除了问题
首先打开开发者工具,找到network栏,再刷新一下网页,就会看到具体的网络请求了
找到对应的方法之后,发现是302错误,也就是重定向错误,此时就可以确认是拦截器的问题了
之前在拦截器中设置了blog_content.html是可以访问的,现在添加了一个/user/getuserbyid接口,会被拦截,所以就会跳转到登录页面
使用fiddler抓包也可以看出来
所以只要在拦截器中添加一条排除/user/getuserbyid接口的规则就行了
实现阅读量累计
要想实现阅读量累加,可以有两种方式
方式一:首先在数据库中查询当前的阅读量,之后再加一
但是,这种方式是很有问题的,要是A和B同时去访问一篇博客,A拿到了阅读量为x,同时B也拿到了阅读量x,在A之后阅读量就会变成x+1,但是B之后,阅读量还是x+1,可实际上应该是x+2才对,所以这个方式的问题在于并发,不能保证操作的原子性
所以采用第二种方式:直接就在数据库中+1,只进行这一步操作就能有效避免非原子性
ArticleMapper:
//将阅读量+1
int updateRcount(@Param("id") Integer id);
ArticleMapper.xml :
<update id="updateRcount">update articleinfo set rcount = rcount+1 where id = #{id};
</update>
ArticleService :
public int updateRcount(Integer id){return articleMapper.updateRcount(id);
}
ArticleController :
//实现阅读量+1
@RequestMapping("addrount")
public AjaxResult updateRcount(Integer id){if(id == null || id <= 0){return AjaxResult.fail(-1,"非法请求");}return AjaxResult.success(articleService.updateRcount(id));
}
和上面的问题一样,要让拦截器不要拦截 /art/addrcount 接口
代码运行之后就可以实现阅读量的累加
新增文章
前端的部分代码:
function mysub(){if(confirm("确认提交吗?")){//1.首先进行非空校验var title = jQuery("#title");if(title.val() == ""){alert("请先输入标题!");title.focus();return;}if(editor.getValue()== ""){alert("请先输入文章内容");return;}//2.请求后端进行博客的添加操作jQuery.ajax({url:"art/add",type:"POST",data:{"title":title.val(),"content":editor.getValue()},success:function(result){if(result != null && result.code == 200 && result.data==1){if(confirm("添加文章成功!是否继续添加?")){//继续添加文章就刷新写博客页面location.href = location.href;}else{//不再添加文章就返回到列表页location.href = "/myblog_list.html";}}else{alert("抱歉,添加文章失败,请重试!");}} })}}
ArticleMapper :
//添加文章
//这里最好还是传articleinfo对象,后续要是改需求也会比较灵活
int add(ArticleInfo articleInfo);
ArticleMapper.xml :
<insert id="add">insert into articleinfo(title,content,uid,createtime) values(#{title},#{content},#{uid},now());
</insert>
ArticleService :
public int add(ArticleInfo articleInfo){return articleMapper.add(articleInfo);
}
ArticleController:
//增加新的文章
@RequestMapping("/add")
public AjaxResult add(HttpServletRequest request , ArticleInfo articleInfo) {//1.进行非空校验,要是articleinfo对象或者标题或者内容为空,那就是非法访问if(articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())|| !StringUtils.hasLength(articleInfo.getContent())){return AjaxResult.fail(-1,"非法请求");}//2.在数据库中新增//新增文章的前提是要知道当前登录的用户的uidUserInfo userInfo = UserSessionUtils.getUser(request);//先得到当前登录用户if(userInfo == null || userInfo.getId()<=0){return AjaxResult.fail(-2,"无效的登录用户");}articleInfo.setUid(userInfo.getId());return AjaxResult.success(articleService.add(articleInfo));
}
这个时候就实现了博客的创建
小优化
有时候文章会很长,在博客列表页中应该显示前一小段的内容,所以要进行截取
在ArticleController文件中:
使用一个foreach来让每篇比较长的博客都显示前100个字
@RequestMapping("/getlist")
public AjaxResult getMyList(HttpServletRequest request) {UserInfo userInfo = UserSessionUtils.getUser(request);if (userInfo == null) {//说明当前不存在session,也就是没有登录(虽然存在拦截器,但是还是要进行判断一下是否是真的登录了)return AjaxResult.fail(-1, "非法请求");}//说明已经正常登录了//这里涉及到了userinfo和articleinfo两张表的关系了//在articleinfo表中id表示每篇文章对应的id,uid表示用户信息//在userinfo中id表示用户的信息,所以userinfo中id对应着articleinfo表中uid,所以这里使用的是userInfo.getId()List<ArticleInfo> list = articleService.getMyList(userInfo.getId());for (ArticleInfo listArr : list) {if(listArr.getContent().length() > 200){listArr.setContent(listArr.getContent().substring(0,100));}}return AjaxResult.success(list);
}
在博客列表页面中,markdown还是以文本的形式来显示的
所以要将markdown渲染成html的形式
首先引入两个依赖:
<dependency><groupId>com.vladsch.flexmark</groupId><artifactId>flexmark-all</artifactId><version>0.36.8</version>
</dependency>
<dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.13.1</version>
</dependency>
在common包下面建一个MarkdownToHTML类:
package com.example.demo.common;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import org.jsoup.Jsoup;
public class MarkdownToHTML {public static String MtoH(String markdown) {// 1: Convert Markdown to HTMLParser parser = Parser.builder().build();HtmlRenderer renderer = HtmlRenderer.builder().build();String html = renderer.render(parser.parse(markdown));// 2: Remove Image tags from HTMLhtml = html.replaceAll("<img[^>]*>", "");// 3: Extract plain text from HTMLString text = Jsoup.parse(html).text();// 4: Output the resultsSystem.out.println(text);return text;}
}
最后在上面的ArticleController类中的 foreach中加入一个调用
@RequestMapping("/getlist")
public AjaxResult getMyList(HttpServletRequest request) {UserInfo userInfo = UserSessionUtils.getUser(request);if (userInfo == null) {//说明当前不存在session,也就是没有登录(虽然存在拦截器,但是还是要进行判断一下是否是真的登录了)return AjaxResult.fail(-1, "非法请求");}//说明已经正常登录了//这里涉及到了userinfo和articleinfo两张表的关系了//在articleinfo表中id表示每篇文章对应的id,uid表示用户信息//在userinfo中id表示用户的信息,所以userinfo中id对应着articleinfo表中uid,所以这里使用的是userInfo.getId()List<ArticleInfo> list = articleService.getMyList(userInfo.getId());for (ArticleInfo listArr : list) {//进行调用listArr.setContent(MarkdownToHTML.MtoH(listArr.getContent()));if(listArr.getContent().length() > 200){listArr.setContent(listArr.getContent().substring(0,100) + "......");}}return AjaxResult.success(list);
}
修改文章
ArticleMapper:
//修改文章
int update(ArticleInfo articleInfo);
ArticleMapper.xml :
<update id="update">update articleinfo set title = #{title},content = #{content}, updatetime = #{updatetime}where id = #{id} and uid = #{uid};
</update>
ArticleService :
public int update(ArticleInfo articleInfo){return articleMapper.update(articleInfo);
}
ArticleController :
//修改文章
@RequestMapping("update")
public AjaxResult update(HttpServletRequest request, ArticleInfo articleInfo) {//首先要进行非空校验//注意:这里还有多判断一个id是否为空,修改文章就要确保传过来的文章id不是空的if(articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())|| !StringUtils.hasLength(articleInfo.getContent())||articleInfo.getId() == null ) {return AjaxResult.fail(-1,"非法请求");}//得到当前登录用户的idUserInfo userInfo = UserSessionUtils.getUser(request);if(userInfo == null || userInfo.getId() <= 0){return AjaxResult.fail(-2,"无效用户");}//修改文章的核心代码!将登录用户的id赋值给articleinfo的uid中,这样执行SQL的时候就会使用登录用户的id了//要是执行SQL的时候,发现数据库中文章与uid对不上,就不会执行修改操作了articleInfo.setUid(userInfo.getId());//添加修改时间articleInfo.setUpdatetime(LocalDateTime.now());return AjaxResult.success(articleService.update(articleInfo));
}
加盐算法
关于加盐算法,可以看看我的另一篇文章
加盐算法的实现思路和具体代码
实现文章的分页功能
最终要实现的四个小功能(首页、上一页、下一页、末页)能分页显示所有的用户写的文章
分页的关键分析:
前端要获取:当前的页数【此时固定每页显示2条文章】
后端要获取:当前的页数、每页中显示最大的条数
显示第一页: select * from articleinfo limit 2;
显示第二页: select * from articleinfo limit 2 offset 2;
显示第三页: select * from articleinfo limit 2 offset 4;
显示第四页: select * from articleinfo limit 2 offset 6;
在写SQL分页语句的时候:offset后面的数字就是跳过多少条文章,在显示第二页的时候就要跳过前两篇文章,显示第三页的时候就要跳过前4篇文章
所以关于分页的规律也就出来了: offest x , 这个x = (页码-1)* 每页显示的最大文章数
在搞定了offset的公式之后,就要考虑一下怎么计算出一共有多少页数
- 拿到所有的文章数
- 文章总数 / 每页显示的最大的文章数(涉及到精度丢失的问题)
- 面对小数怎么应对?
//获取当前一共要有多少页
//首先求出总共有多少条数据
int Count = articleService.getCount();
double temp = Count/(psize * 1.0);//变成double,避免使用int造成精度丢失
int pages = (int) Math.ceil(temp);//ceil的方法的作用就是将小数向上提成整数,eg:2.2-->3,要是已经是整数的话就不会改变
开始写后端的代码:
ArticleMapper:
//分页显示文章
//这里的psize是每页显示的最大条数,offsize就是offset后面的数字,由于offset是关键字所以不方便直接使用
List<ArticleInfo> getListByPage(@Param("psize") Integer psize,@Param("offsize") Integer offsize);//查询一共有多少篇文章
int getCount();
ArticleMapper.xml :
<select id="getListByPage" resultType="com.example.demo.entity.ArticleInfo">select * from articleinfo limit #{psize} offset #{offsize};
</select><select id="getCount" resultType="Integer">select count(*) from articleinfo;
</select>
ArticleService:
public List<ArticleInfo> getListByPage(Integer psize,Integer offsize){return articleMapper.getListByPage(psize,offsize);
}public int getCount(){return articleMapper.getCount();
}
ArticleController:
/* 根据分页查询列表* @param pindex 当前要显示的页码(从1开始,至少为1)* @param psize 每页最大显示的文章数(至少为1)* @return*/
@RequestMapping("/listbypage")
public AjaxResult getListByPage(Integer pindex,Integer psize){//参数校正,在首页的时候不传参数的话,我要自己手动设置if(pindex == null || pindex < 1){pindex = 1;//页码至少是1}if(psize == null || psize < 1){psize = 2;//要是不传每页最大显示数,那就设置成2}//分页的公式: offset = (页码-1)*每页最大的显示数int offset = (pindex -1 )* psize;List<ArticleInfo> list = articleService.getListByPage(psize,offset);//获取当前一共要有多少页//首先求出总共有多少条数据int Count = articleService.getCount();double temp = Count/(psize * 1.0);//变成double,避免使用int造成精度丢失int pages = (int) Math.ceil(temp);//ceil的方法的作用就是将小数向上提成整数,eg:2.2-->3,要是已经是整数的话就不会改变HashMap<String,Object> hashMap = new HashMap<>();//键值对的形式传进去hashMap.put("list",list);hashMap.put("pages",pages);return AjaxResult.success(hashMap);
}
这就是后端的代码实现,其实分页功能最难的就是找出offset 每页显示的最大文章数 当前的页码 三者之间的关系
代码运行的时候发现报错了,使用network抓包之后才发现是一个已经很常见的问题: 被拦截器拦截了,所以只要在AppConfig中添加.excludePathPatterns(“/art/listbypage”),不要拦截listbypage接口就行了
Session持久化
现在已经将主要的功能实现了,但是每次重启服务器都要重新输入账号和密码,也就是说session并不能持久保存,可以使用Redis来持久化保存session
首先要引入redis和session对应的依赖
<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>
之后在properties:
spring.session.store-type=redis
spring.redis.host=47.96.166.241
spring.redis.port=6379
#spring.redis.password= 要是Redis没有密码可以不写
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session
这样子就完成了将session存储到Redis中,以后重启服务器也不会丢失session
以上就是我的SSM版本的博客系统的所有功能的实现。