【springBoot】博客系统

SSM版本的博客系统

1. 项目亮点

  1. 使用MD5+加盐算法进行密码的加密
  2. 使用Redis持久化存储Session
  3. 使用拦截器验证用户登录

2. 项目创建

1.项目框架的选择

image-20231022135548585

2. 项目依赖的引入

image-20231022135718849

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

image-20231022141225593

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. 项目分层

image-20231022145733864

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">查看全文 &gt;&gt;</a>&nbsp;&nbsp;';artListDiv += '<a href="blog_edit.html?id='+artItem.id + '" class="detail">修改 &gt;&gt;</a>&nbsp;&nbsp;';artListDiv += '<a href="javascript:myDel('+artItem.id+');" class="detail">删除 &gt;&gt;</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);}
}

设置返回时间格式:

  1. 在yml中设置全局配置
spring:jackson:date-format: 'yyyy-MM-dd HH:mm:ss'time-zone: 'GMT+8'

这种方式的问题是针对LocalDateTime不起作用,对Date变量才起作用

  1. 使用@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> &nbsp;&nbsp;阅读量:<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.自实现加密的工具类

加盐加密的实现思路:

  1. 加密的思路:每次调用方法时产生唯一的盐值 + 密码 = 最终密码(MD5加密)
  2. 解密思路:需要两个密码:需要验证的密码 + 最终加密的密码
    • 核心思想:得到盐值。将盐值存放到最终密码的某个为止,然后在最终密码中得到盐值
    • 通过: 需要验证的密码+盐值,走一遍加密同样的过程得到的密码和最终密码比较

自己定义一个工具类,其中定义如何进行加密和解密的方法

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进行加盐加密

  1. 引入spring security框架

1698306874541

spring security有自己的认证体系,我们对其进行访问的时候,必须先登录spring security的登录界面

image-20231026155711227

产生这个的原因是:这个加入框架之后,是会产生spring boot的自动注入的。

  1. 排除spring security自动加载

在启动类上加:@SpringBootApplication(exclude = SecurityAutoConfiguration.class)

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class MycnblogApplication {public static void main(String[] args) {SpringApplication.run(MycnblogApplication.class, args);}
}
  1. 调用 实现加盐和验证
    @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">查看全文 &gt;&gt;</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;
}

上述就实现了博客系统最基本的功能了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/151664.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小米14系列, OPPO Find N3安装谷歌服务框架,安装Play商店,Google

10月26号小米发布了新款手机小米14,那么很多大家需求问是否支持谷歌服务框架,是否支持Google Play商店gms。因为毕竟小米公司现在安装的系统是HyperOS澎湃OS。但是我拿到手机之后会发现还是开机初始界面会显示power by android,证明这一点他还是支持安装谷歌,包括最近一段时间发…

数据库连接技术

一、许多编程语言 都可以 连接数据库。不是在C中加入SQL语句&#xff0c;而是使 用C编程语言 连接数据库&#xff0c;并执行SQL语句&#xff0c;以获得数据。 数据库连接&#xff0c;有一些通用的方式。C中连接数据库并执行SQL语句&#xff0c;主要有以下几种方式&#xff1a; …

使用Python将PDF转为图片

将PDF转为图片能方便我们将文档内容上传至社交媒体平台进行分享。此外&#xff0c;转换为图片后&#xff0c;还可以对图像进行进一步的裁剪、调整大小或添加标记等操作。 用Python将PDF文件转JPG/ PNG图片可能是大家在一些项目中会遇到的需求&#xff0c;下面将详细介绍如何使用…

ASEMI高压二极管CL08-RG210参数,CL08-RG210封装

编辑-Z CL08-RG210参数描述&#xff1a; 型号&#xff1a;CL08-RG210 反向重复峰值电压VRRM&#xff1a;8000V 反向工作峰值电压VRWM&#xff1a;8000V 正向平均电流IF&#xff1a;0.5A 正向(不重复)浪涌电流IFSM&#xff1a;20A 反向恢复时间trr&#xff1a;80ns 正向…

如何在Postman中使用静态HTTP

首先&#xff0c;打开 Postman 软件。在 Postman 的菜单栏中&#xff0c;点击 “Preferences”&#xff08;偏好设置&#xff09;。 亲身经验&#xff1a;我自己尝试了这个方法&#xff0c;发现它非常适用于需要使用HTTP的场景。 数据和引证&#xff1a;根据 Postman 官方文档…

Java NIO 高并发开发

Java NIO 高并发开发 前言 Java NIO&#xff08;New I/O&#xff09;相比于传统的Java I/O&#xff08;BIO&#xff09;在高并发开发方面具有以下优势&#xff1a; 非阻塞模式&#xff1a;Java NIO使用非阻塞的I/O操作&#xff0c;允许一个线程管理多个通道&#xff08;Channe…

systrace/perfetto如何看surfaceflinger的vsync信号方法-android framework实战车载手机系统开发

背景&#xff1a; hi&#xff0c;粉丝朋友们&#xff1a; 大家好&#xff01;近期分享了surfaceflinger相关的一些blog&#xff0c;有同学就对相关的一些内容产生了一些疑问。 比如&#xff1a;vsync查看问题&#xff0c;即怎么才可以说是vsync到来了。 比如perfetto中surfac…

打破信息孤岛,如何从API、数据中台突围

“烟囱”林立&#xff0c;零售企业“数据孤岛”现象突出 所谓数据孤岛&#xff0c;是指零售企业不同组织机构之间、不同部门之间或不同软件之间的数据无法连接互动&#xff0c;数据信息不能共享&#xff0c;设计、管理、生产的数据不能相互交流&#xff0c;数据出现脱节的现象…

vue项目中内嵌iframe,打包上线时候iframe地址如何写?

vue项目中内嵌iframe&#xff0c;打包上线时候iframe地址如何写 一、项目结构1.内嵌的iframe文件位置2.打包后的iframe的位置 二、代码 前提描述&#xff0c;项目是用webpack打包的&#xff0c;内嵌一个完整的js小组件 一、项目结构 1.内嵌的iframe文件位置 2.打包后的iframe的…

归结原理、归结演绎推理

主要内容 归结演绎推理范式子句与子句集将谓词公式转化为子句集命题逻辑鲁宾逊归结原理 归结演绎推理 定理证明的实质是对前提P和结论Q证明P →Q的永真性应用反证法&#xff0c;欲证明P →Q&#xff0c;只要证明 P∧~Q 等价于 F鲁宾逊归结原理对机械化推理有重大突破鲁宾逊归…

吴恩达《机器学习》1-3:监督学习

一、监督学习 例如房屋价格的数据集。在监督学习中&#xff0c;我们将已知的房价作为"正确答案"&#xff0c;并将这些价格与房屋的特征数据一起提供给学习算法。学习算法使用这些已知答案的数据来学习模式和关系&#xff0c;以便在未知情况下预测其他房屋的价格。这就…

牛客网刷题-(7)

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…