SSM版本的博客系统
1. 项目亮点
- 使用MD5+加盐算法进行密码的加密
- 使用Redis持久化存储Session
- 使用拦截器验证用户登录
2. 项目创建
1.项目框架的选择
2. 项目依赖的引入
3. 静态页面的代码文件:
program/博客系统(静态页面).rar · 叁伍/java语言练习 - 码云 - 开源中国 (gitee.com)
4. 配置文件的书写:
# 配置数据库的连接字符串
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8username: rootpassword: abc123driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:mapper-locations: classpath:mapper/*Mapper.xmlconfiguration: # 配置打印 MyBatis 执行的 SQLlog-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:level:com:example:demo: debug
5. 初始化数据库
-- 创建数据库
drop database if exists mycnblog_ssm;
create database mycnblog_ssm DEFAULT CHARACTER SET utf8mb4;-- 使用数据数据
use mycnblog_ssm;-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(id int primary key auto_increment,username varchar(100) not null unique,password varchar(65) not null,photo varchar(500) default '', createtime timestamp default current_timestamp,updatetime datetime not null,`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 timestamp default current_timestamp,updatetime datetime not null,uid int not null,rcount int not null default 1,`state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog_ssm`.`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, `createtime`, `updatetime`)values('Java','Java正文',1, '2021-12-06 17:10:48', '2021-12-06 17:10:48');
6. 项目分层
7. 统一数据的返回
统一数据返回,在common包中:
@Data
public class AjaxResult implements Serializable {// 状态码private Integer code;// 状态码描述信息private String msg;// 返回的数据private Object data;/*** 操作成功返回的结果*/public static AjaxResult success(Object data) {AjaxResult result = new AjaxResult();result.setCode(200);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(int code, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}/*** 返回失败结果*/public static AjaxResult fail(int code, String msg) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(null);return result;}public static AjaxResult fail(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}}
保底策略:在返回数据之前,检测数据的类型是否为统一的对象,如果不是,封装成统一的对象。在config包中:
@ControllerAdvice//表示控制器通知类
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;/*** 开关,如果是true才会调用beforeBodyWrite*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}/*** 对数据格式进行效验和封装*/@SneakyThrows//异常抛出,相当于方法上throw一个异常@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof AjaxResult){//进行了统一对象的封装,直接返回bodyreturn body;}if (body instanceof String){//字符串类型特殊处理。手动对其进行ajax类型的转化return objectMapper.writeValueAsString(AjaxResult.success(body));}return AjaxResult.success(body);//未进行封装的对其进行统一封装}
}
3. 实现注册功能
1. 前端实现
前端是在reg.html页面进行实现注册页面的。
<div class="login-container"><!-- 中间的注册框 --><div class="login-dialog"><!-- …… --><div class="row"><button id="submit" onclick="mysub()">提交</button></div></div>
</div><script>//提交注册事件function mysub(){// 1.非空效验var username = jQuery("#username");var password = jQuery("#password");var password2 = jQuery("#password2");if(username.val()==""){alert("请先输入用户名!");username.focus(); // 将鼠标光标设置到用户名控件return;}if(password.val()==""){alert("请先输入密码!");password.focus();return;}if(password2.val()==""){alert("请先输入确认密码!");password2.focus();return;}// 2.判断两次密码是否一致if(password.val()!=password2.val()){alert("两次密码输入的不一致,请先检查!");password.focus();return;}// 3.ajax 提交请求jQuery.ajax({url:"/user/reg",//请求地址type:"POST",//请求类型data:{"username":username.val(),"password":password.val()},//请求数据success:function(result){// 响应的结果if(result!=null && result.code==200 && result.data==1){// 执行成功if(confirm("恭喜:注册成功!是否要跳转到登录页面?")){location.href = "/login.html";}}else{alert("抱歉执行失败,请稍后再试!");}}});}
</script>
2. 后端实现
Userinfo实体类的实现(在entity包中实现):
@Data
public class Userinfo {private Integer id;private String username;private String password;private String photo;private LocalDateTime createtime;private LocalDateTime updatetime;private Integer state;
}
在mapper包中定义UserMapper接口:
@Mapper
public interface UserMapper {// 注册int reg(Userinfo userinfo);
}
在resources中的mapper文件夹中创建 UserMapper.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.UserMapper"><insert id="reg">insert into userinfo(username,password,updatetime) values(#{username},#{password},#{updatetime})</insert></mapper>
在 service包中创建UserService类
@Service
public class UserService {@Resourceprivate UserMapper userMapper;public int reg(Userinfo userinfo) {return userMapper.reg(userinfo);}
}
在 controller包中创建UserController类
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/reg")public AjaxResult reg(Userinfo userinfo) {// 非空效验和参数有效性效验if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||!StringUtils.hasLength(userinfo.getPassword())) {return AjaxResult.fail(-1, "非法参数");}userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间return AjaxResult.success(userService.reg(userinfo));}
}
前端注册成功,并且数据库userinfo表中添加了一条对应的数据,证明注册模块已经实现成功
4. 登录功能
前端页面
<div class="login-container"><!-- 中间的注册框 --><div class="login-dialog"><!-- …… --><div class="row"><button id="submit" onclick="mysub()">提交</button></div></div>
</div>
<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",type:"POST",data:{"username":username.val(),"password":password.val()},success:function(result){if(result!=null && result.code==200 && result.data!=null){// 登录成功location.href = "myblog_list.html";}else{alert("抱歉登录失败,用户名或密码输入错误,请重试!");}}});}</script>
后端实现
在userMapper接口中定义抽象方法:
Userinfo getUserByName(@Param("username") String username);
在 UserMapper.xml文件中添加sql语句:
<select id="getUserByName" resultType="com.example.demo.entity.Userinfo">select * from userinfo where username=#{username}
</select>
在userService类中实现getUserByName方法:
public Userinfo getUserByName(String username){return userMapper.getUserByName(username);
}
在UserController类中实现login方法:
实现这方法的时候,我们存储用户登录信息在session中。我们session的key值后续在很多地方都会用到,我们在common包中定义一个全局变量:
public class AppVariable {// 用户 session keypublic static final String USER_SESSION_KEY = "USER_SESSION_KEY";
}
实现login方法:
@RequestMapping("/login")public AjaxResult login(HttpServletRequest request,String username,String password){// 非空效验和参数有效性效验if (username == null || !StringUtils.hasLength(username) ||!StringUtils.hasLength(password) ){return AjaxResult.fail(-1, "非法参数");}// 2.查询数据库Userinfo userinfo = userService.getUserByName(username);if (userinfo != null && userinfo.getId() > 0) { // 有效的用户// 两个密码是否相同if (password.equals(userinfo.getPassword())) {// 登录成功// 将用户存储到 sessionHttpSession session = request.getSession();session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息return AjaxResult.success(userinfo);}}return AjaxResult.success(0,null);}
登录拦截器的实现
在config包中,实现切面类和定义拦截规则
实现切面类:
public class LoginInterceptor implements HandlerInterceptor {/*** true -> 用户已登录* false -> 用户未登录*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {// 用户已登陆System.out.println("当前登录用户为:" +((Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY)).getUsername());return true;}// 调整到登录页面response.sendRedirect("/login.html");return false;}
}
实现拦截规则:
@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");}
}
5.博客列表页的实现
1. 显示当前用户信息
前端实现
<!-- 版心 -->
<div class="container"><!-- 左侧个人信息 --><div class="container-left"><div class="card"><img src="img/doge.jpg" class="avtar" alt=""><h3 id="username"></h3><a href="http:www.github.com">github 地址</a><div class="counter"><span>文章</span><span>分类</span></div><div class="counter"><span id="artCount"></span><span>1</span></div></div></div><!-- 右侧内容详情 -->
</div>
<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);jQuery("#artCount").text(result.data.artCount);}else{alert("个人信息加载失败,请重新刷新再试!");}}});}showInfo();
</script>
后端实现
在mapper包中创建ArticleMapper接口,并且创建getArtCountByUid抽象方法。
@Mapper
public interface ArticleMapper {//查询文章总数int getArtCountByUid(@Param("uid") Integer uid);
}
在static的mapper包中创建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="getArtCountByUid" resultType="Integer">select count(*) from articleinfo where uid=#{uid}</select>
</mapper>
在service包中创建ArticleService类并且在getArtCountByUid方法中调用articleMapper中的getArtCountByUid方法
@Service
public class ArticleService{@Resourceprivate ArticleMapper articleMapper;public int getArtCountByUid(Integer uid) {return articleMapper.getArtCountByUid(uid);}
}
在entity包中定义 UserinfoVO继承Userinfo类,新增artCount属性用来存储查询出的文章数量
@Data
public class UserinfoVO extends Userinfo {private Integer artCount; // 此人发表的文章总数
}
把对当前登录用户相关的操作进行用一个类进行封装
public class UserSessionUtils {/*** 得到当前的登录用户*/public static Userinfo getUser(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session != null &&session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {// 说明用户已经正常登录return (Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY);}return null;}
}
在controller包中的UserController类中定义showInfo方法
@RequestMapping("/showinfo")
public AjaxResult showInfo(HttpServletRequest request) {UserinfoVO userinfoVO = new UserinfoVO();// 1.得到当前登录用户(从 session 中获取)Userinfo userinfo = UserSessionUtils.getUser(request);if (userinfo == null) {return AjaxResult.fail(-1, "非法请求");}// Spring 提供的深克隆方法BeanUtils.copyProperties(userinfo, userinfoVO);// 2.得到用户发表文章的总数userinfoVO.setArtCount(articleService.getArtCountByUid(userinfo.getId()));return AjaxResult.success(userinfoVO);
}
也可以对1号账户新增一片博客进行验证
insert into articleinfo (title,content,updatetime,uid) value ('第二篇文章','第二篇文章,大家好','2021-12-06 17:10:48',1);
2. 获取文章列表的数据
获取自己的文章列表不用传递参数,后端通过session获取用户id。如果传参可能被有心人通过传参获取别人的文章。
前端实现
// 获取我的文章列表数据
function getMyArtList(){jQuery.ajax({url:"/art/mylist",type:"POST",data:{},success:function(result){if(result!=null && result.code==200){// 有两种情况,一种是发表了文章,一种是没有发表任何文章 if(result.data!=null && result.data.length>0){// 此用户发表文章了var artListDiv ="";for(var i=0;i<result.data.length;i++){var artItem = result.data[i];artListDiv += '<div class="blog">';artListDiv += '<div class="title">'+artItem.title+'</div>';artListDiv += '<div class="date">'+artItem.updatetime+'</div>';artListDiv += '<div class="desc">';artListDiv += artItem.content;artListDiv += '</div>';artListDiv += '<a href="blog_content.html?id='+artItem.id + '" class="detail">查看全文 >></a> ';artListDiv += '<a href="blog_edit.html?id='+artItem.id + '" class="detail">修改 >></a> ';artListDiv += '<a href="javascript:myDel('+artItem.id+');" class="detail">删除 >></a>';artListDiv += '</div>';}jQuery("#artDiv").html(artListDiv);}else{// 当前用户从未发过任何文章jQuery("#artDiv").html("<h3>暂无文章~</h3>");}}else{alert("查询文章列表出错,请重试!");}}});
}
getMyArtList();
// 删除文章
function myDel(id){}
上述完成后右侧内容详情删除详情页的信息,留下以下内容:
<!-- 右侧内容详情 -->
<div id="artDiv" class="container-right"><!-- 每一篇博客包含标题, 摘要, 时间 -->
</div>
后端实现
在entity包中定义一个Articleinfo实体类
@Data
public class Articleinfo {private Integer id;private String title;private String content;private LocalDateTime createtime;private LocalDateTime updatetime;private Integer uid;private Integer rcount;private Integer state;
}
在mapper包中的ArticleMapper接口中定义getMyList方法
List<Articleinfo> getMyList(@Param("uid") Integer uid);
在static中的mapper包中的ArticleMapper.xml文件中定义书写内容
<select id="getMyList" resultType="com.example.demo.entity.Articleinfo">select * from articleinfo where uid=#{uid}
</select>
在service包中的ArticleService接口中定义getMyList方法
public List<Articleinfo> getMyList(Integer uid) {return articleMapper.getMyList(uid);
}
在controller包中定义ArticleController类并且在内部定义getMyList方法
@RestController
@RequestMapping("/art")
public class ArticleController {@Autowiredprivate ArticleService articleService;@RequestMapping("/mylist")public AjaxResult getMyList(HttpServletRequest request) {Userinfo userinfo = UserSessionUtils.getUser(request);if (userinfo == null) {return AjaxResult.fail(-1, "非法请求");}List<Articleinfo> list = articleService.getMyList(userinfo.getId());return AjaxResult.success(list);}
}
设置返回时间格式:
- 在yml中设置全局配置
spring:jackson:date-format: 'yyyy-MM-dd HH:mm:ss'time-zone: 'GMT+8'
这种方式的问题是针对LocalDateTime不起作用,对Date变量才起作用
- 使用@JsonFormat注解来返回时间格式
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime createtime;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime updatetime;
6. 注销和删除博客功能
1. 注销登录的前端实现
<a href="javascript:logout()">注销</a>
<script>function logout(){if(confirm("确认注销登录?")){jQuery.ajax({url:"/user/logout",type:"POST",data:{},success:function(res){if(res != null && res.code == 200){location.href = "/login.html";}else{alert("抱歉:操作失败,请重试!"+res.msg);}}});}}
</script>
2. 注销登录的后端实现
在UserController类中实现logout方法:
/*** 注销(退出登录)*/
@RequestMapping("/logout")
public AjaxResult logout(HttpSession session) {session.removeAttribute(AppVariable.USER_SESSION_KEY);return AjaxResult.success(1);
}
3. 删除博客功能前端实现
在博客详情页(myblog_list.html)中实现下面功能:
需要注意的时不能传递用户id参数,登录用户信息靠后端从session中获取:
<a href="javascript:logout()">注销</a>
<script>// 删除文章function myDel(id){if(confirm("确实删除?")){// 删除文章jQuery.ajax({url:"art/del",type:"POST",data:{"id":id},success:function(result){if(result!=null && result.code==200 && result.data==1){alert("恭喜:删除成功!");// 刷新当前页面location.href = location.href;}else{alert("抱歉:删除失败,请重试!");}}});}}
</script>
4. 删除博客功能后端实现
在ArticleMapper接口定义del抽象方法
int del(@Param("id") Integer id, @Param("uid") Integer uid);
在ArticleMapper.xml中书写sql语句
<delete id="del">delete from articleinfo where id=#{id} and uid=#{uid}
</delete>
service包中的ArticleService类实现del方法
public int del(Integer id, Integer uid) {return articleMapper.del(id, uid);
}
在controller包中的ArticleController类下实现 del方法
@RequestMapping("/del")
public AjaxResult del(HttpServletRequest request, Integer id) {if (id == null || id <= 0) {// 参数有误return AjaxResult.fail(-1, "参数异常");}Userinfo user = UserSessionUtils.getUser(request);if (user == null) {return AjaxResult.fail(-2, "用户未登录");}return AjaxResult.success(articleService.del(id, user.getId()));
}
7. 博客详情页
1.展示博客内容前端实现
<!-- 右侧内容详情 -->
<div class="container-right"><div class="blog-content"><!-- 博客标题 --><h3 id="title"></h3><!-- 博客时间 --><div class="date">发布时间:<span id="updatetime"></span> 阅读量:<span id="rcount"></span></div><!-- 博客正文 --><div id="editorDiv"></div></div>
</div>
<script>// 获取当前url参数的方法function getUrlValue(key){// ex:?id=1&v=2var params = location.search;if(params.length>1){// ex:id=1&v=2params = location.search.substring(1);var paramArr = params.split("&");for(var i=0;i<paramArr.length;i++){var kv = paramArr[i].split("=");if(kv[0]==key){// 是我要查询的参数return kv[1];}}}return "";}// 查询文章详情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);}else{alert("查询失败,请重试!");}}});}getArtDetail(getUrlValue("id"));
</script>
2.展示博客内容后端实现
在ArticleMapper接口定义getDetail抽象方法
Articleinfo getDetail(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句
<select id="getDetail" resultType="com.example.demo.entity.Articleinfo">select * from articleinfo where id=#{id}
</select>
service包中的ArticleService类实现getDetail方法
public Articleinfo getDetail(Integer id) {return articleMapper.getDetail(id);
}
在controller包中的ArticleController类下实现 getDetail方法
@RequestMapping("/detail")
public AjaxResult getDetail(Integer id) {if (id == null || id <= 0) {return AjaxResult.fail(-1, "非法参数");}return AjaxResult.success(articleService.getDetail(id));
}
3.详情页作者信息展示前端
<!-- 左侧个人信息 -->
<div class="container-left"><div class="card"><img src="img/avatar.png" class="avtar" alt=""><h3 id="username"></h3><a href="http:www.github.com">github 地址</a><div class="counter"><span>文章</span><span>分类</span></div><div class="counter"><span id="artCount"></span><span>1</span></div></div>
</div>
<script>//此方法在getArtDetail方法中查询成功后调用showUser(result.data.uid);// 查询用户的详情信息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("抱歉:查询用户信息失败,请重试!");}}});}
</script>
4.详情页作者信息展示后端
在ArticleMapper接口定义getUserById抽象方法
Userinfo getUserById(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句
<select id="getUserById" resultType="com.example.demo.entity.Userinfo">select * from userinfo where id=#{id}
</select>
service包中的ArticleService类实现getUserById方法
public Userinfo getUserById(Integer id) {return userMapper.getUserById(id);
}
在controller包中的ArticleController类下实现 getUserById方法
@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(-1, "非法参数");}// 去除 userinfo 中的敏感数据,ex:密码userinfo.setPassword("");UserinfoVO userinfoVO = new UserinfoVO();BeanUtils.copyProperties(userinfo, userinfoVO);// 查询当前用户发表的文章数userinfoVO.setArtCount(articleService.getArtCountByUid(id));return AjaxResult.success(userinfoVO);
}
5. 实现增加阅读量前端
<script>// 阅读量 +1function updataRCount(){// 先得到文章 idvar id = getUrlValue("id");if(id!=""){jQuery.ajax({url:"/art/incr-rcount",type:"POST",data:{"id":id},success:function(result){}});}}updataRCount();
</script>
6.实现增加阅读量后端
在ArticleMapper接口定义incrRCount抽象方法
public int incrRCount(@Param("id") Integer id);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<update id="incrRCount">update articleinfo set rcount=rcount+1 where id=#{id}
</update>
service包中的ArticleService类实现incrRCount方法
public int incrRCount(Integer id) {return articleMapper.incrRCount(id);
}
在controller包中的ArticleController类下实现 incrRCount方法
@RequestMapping("/incr-rcount")
public AjaxResult incrRCount(Integer id) {if (id != null && id > 0) {return AjaxResult.success(articleService.incrRCount(id));}return AjaxResult.fail(-1, "未知错误");
}
8. 新增文章页
前端实现
<!-- 编辑框容器 -->
<div class="blog-edit-container"><!-- 标题编辑区 --><div class="title"><input type="text" id="title" placeholder="在这里写下文章标题"><button onclick="mysub()">发布文章</button></div><!-- 创建编辑器标签 --><div id="editorDiv"><textarea id="editor-markdown" style="display:none;"></textarea></div>
</div>
<script>// 提交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("抱歉,文章添加失败,请重试!");}}});}}
</script>
后端实现
在ArticleMapper接口定义add抽象方法
int add(Articleinfo articleinfo);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<insert id="add">insert into articleinfo(title,content,uid,updatetime) values(#{title},#{content},#{uid},#{updatetime})
</insert>
service包中的ArticleService类实现add方法
public int add(Articleinfo articleinfo) {return articleMapper.add(articleinfo);
}
在controller包中的ArticleController类下实现 add方法
@RequestMapping("/add")
public AjaxResult add(HttpServletRequest request, Articleinfo articleinfo) {// 1.非空效验if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||!StringUtils.hasLength(articleinfo.getContent())) {// 非法参数return AjaxResult.fail(-1, "非法参数");}// 2.数据库添加操作// a.得到当前登录用户的 uidUserinfo userinfo = UserSessionUtils.getUser(request);if (userinfo == null || userinfo.getId() <= 0) {// 无效的登录用户return AjaxResult.fail(-2, "无效的登录用户");}articleinfo.setUid(userinfo.getId());articleinfo.setUpdatetime(LocalDateTime.now());//设置更新时间// b.添加数据库并返回结果return AjaxResult.success(articleService.add(articleinfo));
}
9. 博客修改页
前端实现
博客编辑页的初始化功能复用之前的博客详情页写的接口即可实现。需要新增的后端接口就是修改操作。
<!-- 编辑框容器 -->
<div class="blog-edit-container"><!-- 标题编辑区 --><div class="title"><input id="title" type="text"><button onclick="mysub()">修改文章</button></div><!-- 创建编辑器标签 --><div id="editorDiv"><textarea id="editor-markdown" style="display:none;"></textarea></div>
</div>
<script>// 提交function mysub(){// 1.非空效验var title = jQuery("#title");if(title.val()==""){alert("请先输入标题!");title.focus();return;}if(editor.getValue()==""){alert("请先输入正文!");return;}// 2.进行修改操作jQuery.ajax({url:"/art/update",type:"POST",data:{"id":id,"title":title.val(),"content":editor.getValue()},success:function(result){if(result!=null && result.code==200 && result.data==1){alert("恭喜:修改成功!");location.href = "myblog_list.html";}else{alert("抱歉:操作失败,请重试!");}}});}// 文章初始化function initArt(){// 得到当前页面 url 中的参数 id(文章id)id = getUrlValue("id");if(id==""){alert("无效参数");location.href = "myblog_list.html";return;}// 请求后端,查询文章的详情信息jQuery.ajax({url:"art/detail",type:"POST",data:{"id":id},success:function(result){if(result!=null && result.code==200){jQuery("#title").val(result.data.title);initEdit(result.data.content);}else{alert("查询失败,请重试!");}}});}initArt();
</script>
后端实现
在ArticleMapper接口定义update抽象方法
int update(Articleinfo articleinfo);
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<update id="update">update articleinfo set title=#{title},content=#{content},updatetime=#{updatetime}where id=#{id} and uid=#{uid}
</update>
service包中的ArticleService类实现update方法
public int update(Articleinfo articleinfo) {return articleMapper.update(articleinfo);
}
在controller包中的ArticleController类下实现 update方法
@RequestMapping("/update")
public AjaxResult update(HttpServletRequest request, Articleinfo articleinfo) {// 非空效验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() == null) {// 无效用户return AjaxResult.fail(-2, "无效用户");}// 很核心的代码(解决了修改文章归属人判定的问题)articleinfo.setUid(userinfo.getId());articleinfo.setUpdatetime(LocalDateTime.now());return AjaxResult.success(articleService.update(articleinfo));
}
10. 密码MD5加盐加密
1.自实现加密的工具类
加盐加密的实现思路:
- 加密的思路:每次调用方法时产生唯一的盐值 + 密码 = 最终密码(MD5加密)
- 解密思路:需要两个密码:需要验证的密码 + 最终加密的密码
- 核心思想:得到盐值。将盐值存放到最终密码的某个为止,然后在最终密码中得到盐值
- 通过: 需要验证的密码+盐值,走一遍加密同样的过程得到的密码和最终密码比较
自己定义一个工具类,其中定义如何进行加密和解密的方法
public class PasswordUtils {/*** 1.加盐并生成密码* @param password 明文密码* @return 保存到数据库中的密码*/public static String encrypt(String password) {// 1.产生盐值(32位)String salt = UUID.randomUUID().toString().replace("-", "");// 2.生成加盐之后的密码String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 3.生成最终密码(保存到数据库中的密码)【约定格式:32位盐值+$+32位加盐之后的密码】String finalPassword = salt + "$" + saltPassword;return finalPassword;}/*** 2.生成加盐的密码(方法1的重载)* @param password 明文* @param salt 固定的盐值* @return 最终密码*/public static String encrypt(String password, String salt) {// 1.生成一个加盐之后的密码String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 2.生成最终的密码【约定格式:32位盐值+$+32位加盐之后的密码】String finalPassword = salt + "$" + saltPassword;return finalPassword;}/*** 3.验证密码* @param inputPassword 用户输入的明文密码* @param finalPassword 数据库保存的最终密码* @return*/public static boolean check(String inputPassword, String finalPassword) {if (StringUtils.hasLength(inputPassword) && StringUtils.hasLength(finalPassword) &&finalPassword.length() == 65) {// 1.得到盐值String salt = finalPassword.split("\\$")[0];// 2.使用之前加密的步骤,将明文密码和已经得到的盐值进行加密,生成最终的密码String confirmPassword = PasswordUtils.encrypt(inputPassword, salt);// 3.对比两个最终密码是否相同return confirmPassword.equals(finalPassword);}return false;}
}
2. 在注册和登录页面修改加密
主要是对UserController类中的reg方法和login方法进行修改
@RequestMapping("/reg")
public AjaxResult reg(Userinfo userinfo) {// 非空效验和参数有效性效验if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||!StringUtils.hasLength(userinfo.getPassword())) {return AjaxResult.fail(-1, "非法参数");}// 密码加盐处理userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间return AjaxResult.success(userService.reg(userinfo));
}
@RequestMapping("/login")
public AjaxResult login(HttpServletRequest request,String username,String password){// 非空效验和参数有效性效验if (username == null || !StringUtils.hasLength(username) ||!StringUtils.hasLength(password) ){return AjaxResult.fail(-1, "非法参数");}// 2.查询数据库Userinfo userinfo = userService.getUserByName(username);if (userinfo != null && userinfo.getId() > 0) { // 有效的用户// 两个密码是否相同if (PasswordUtils.check(password, userinfo.getPassword())) {// 登录成功// 将用户存储到 sessionHttpSession session = request.getSession();session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息return AjaxResult.success(userinfo);}}return AjaxResult.success(0,null);
}
3. 使用spring security进行加盐加密
- 引入spring security框架
spring security有自己的认证体系,我们对其进行访问的时候,必须先登录spring security的登录界面
产生这个的原因是:这个加入框架之后,是会产生spring boot的自动注入的。
- 排除spring security自动加载
在启动类上加:@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class MycnblogApplication {public static void main(String[] args) {SpringApplication.run(MycnblogApplication.class, args);}
}
- 调用 实现加盐和验证
@Testvoid testSecurity(){BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();String password = "123456";String finalPassword = passwordEncoder.encode(password);System.out.println(finalPassword);System.out.println(passwordEncoder.encode(password));System.out.println(passwordEncoder.encode(password));//解密:参数1:明文密码。参数2:最终密码String inputPassword = "12345";System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword,finalPassword));String inputPassword2 = "123456";System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword2,finalPassword));}
在测试类中实现上面的测试方法。发现每次加盐的结果都是不同的。
11. 分页展示实现
1.分页功能的后端
分页公式的值 = (当前页码 - 1) * 每页显示的条数
在ArticleMapper接口定义getListByPage和getCount抽象方法
List<Articleinfo> getListByPage(@Param("psize") Integer psize,@Param("offsize") Integer offsize);
int getCount();
在ArticleMapper.xml中书写sql语句:
在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。
<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>
service包中的ArticleService类实现getListByPage和getCount方法
public List<Articleinfo> getListByPage(Integer psize, Integer offsize) {return articleMapper.getListByPage(psize, offsize);
}
public int getCount() {return articleMapper.getCount();
}
在controller包中的ArticleController类下实现 getListByPage方法
@RequestMapping("/listbypage")
public AjaxResult getListByPage(Integer pindex, Integer psize) {// 1.参数校正if (pindex == null || pindex <= 1) {pindex = 1;}if (psize == null || psize <= 1) {psize = 2;}// 分页公式的值 = (当前页码-1)*每页显示条数int offset = (pindex - 1) * psize;// 文章列表数据List<Articleinfo> list = articleService.getListByPage(psize, offset);// 当前列表总共有多少页// a.总共有多少条数据int totalCount = articleService.getCount();// b.总条数/psize(每页显示条数)double pcountdb = totalCount / (psize * 1.0);// c.使用进一法得到总页数int pcount = (int) Math.ceil(pcountdb);HashMap<String, Object> result = new HashMap<>();result.put("list", list);result.put("pcount", pcount);return AjaxResult.success(result);
}
2. 分页功能的前端
版心部分的html代码:
<!-- 版心 -->
<div class="container"><!-- 右侧内容详情 --><div class="container-right" style="width: 100%;"><div id="artListDiv"></div><hr><div class="blog-pagnation-wrapper"><button onclick="goFirstPage()" class="blog-pagnation-item">首页</button><button onclick="goBeforePage()" class="blog-pagnation-item">上一页</button><button onclick="goNextPage()" class="blog-pagnation-item">下一页</button><button onclick="goLastPage()" class="blog-pagnation-item">末页</button></div></div>
</div>
js代码
// 当前页码
var pindex = 1;
// 每页显示条数
var psize = 2;
// 最大页数
var pcount =1;
// 1.先尝试得到当前 url 中的页码
pindex = (getUrlValue("pindex")==""?1:getUrlValue("pindex"));
// 2.查询后端接口得到当前页面的数据,进行展示
function initPage(){jQuery.ajax({url:"/art/listbypage",type:"POST",data:{"pindex":pindex,"psize":psize},success:function(result){if(result!=null && result.code==200 && result.data.list.length>0){var artListHtml = "";for(var i=0;i<result.data.list.length;i++){var articleinfo = result.data.list[i];artListHtml +='<div class="blog">';artListHtml +='<div class="title">'+articleinfo.title+'</div>';artListHtml +='<div class="date">'+articleinfo.updatetime+'</div>';artListHtml +='<div class="desc">'+articleinfo.content+'</div>';artListHtml +='<a href="blog_content.html?id='+ articleinfo.id+'" class="detail">查看全文 >></a>';artListHtml +='</div>'; }jQuery("#artListDiv").html(artListHtml);pcount = result.data.pcount;}}});
}
initPage();
// 跳转到首页
function goFirstPage(){if(pindex<=1){alert("当前已经在首页了");return;}location.href = "blog_list.html";
}
// 点击上一页按钮
function goBeforePage(){if(pindex<=1){alert("当前已经在首页了");return;}pindex = parseInt(pindex) -1;location.href ="blog_list.html?pindex="+pindex;
}
function goNextPage(){if(pindex>=pcount){alert("已经在末页了");return; }pindex = parseInt(pindex)+1;location.href ="blog_list.html?pindex="+pindex;
}
function goLastPage(){if(pindex>=pcount){alert("已经在末页了");return; }location.href ="blog_list.html?pindex="+pcount;
}
上述就实现了博客系统最基本的功能了