文章目录
- 项目优点
- 项目创建
- 创建相应的目录,文件,表,导入前端资源
- 实现common工具类
- 实现拦截器验证用户登录
- 实现统一数据返回格式
- 实现加盐加密类
- 实现encrypt方法
- 实现decrypt方法
- 实现SessionUtil类
- 实现注册页面
- 实现前端代码
- 实现后端代码
- 实现登录页面
- 实现前端代码
- 实现后端代码
- 实现个人主页
- 实现退出登录功能
- 实现前端代码
- 实现后端代码
- 初始化个人信息(包括个人文章列表的个人信息)和完成删除功能
- 实现前端代码
- 实现后端代码
- 实现详情页(blog_content.html)
- 实现前端代码
- 实现后端代码
- 实现博客修改功能(blog_update.html)
- 实现前端代码
- 实现后端代码
- 实现博客编辑页
- 实现前端代码
- 实现后端代码
- 实现我的草稿箱(draft_list.html)
- 实现前端代码
- 实现后端代码
- 实现博客主页(blog_list.html)
- 实现前端代码
- 实现后端代码
- 将session持久化到redis
- 其他扩展功能
前言
这个博客系统前端分为8个页面,分别是注册页,登录页,编辑页,修改页,个人主页,博客正文页,草稿列表页,博客列表页
项目优点
- 框架:使用ssm(SpringBoot + SpringMVC + MyBatis)
- 密码:用户登录用的密码是使用加盐算法处理然后存储到数据库
- 用户登录状态持久化:将session持久化到redis
- 功能升级:在博客列表页实现一个分页功能
- 使用拦截器进行用户的登录验证,统一数据返回格式,统一异常处理
项目创建
创建一个SpringBoot项目,添加需要的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
创建相应的目录,文件,表,导入前端资源
首先在数据库建表,这里直接提供sql语句
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid,state)
values('喜讯','今天是正月初六',9,2);
- 实体层(model):创建实体类UserInfo,ArticleInfo
- 控制层(controller):创建控制器UserController和ArticleInfo
- 服务层(servlce):创建服务类:UserService和ArticleService
- 持久层(mapper):创建接口UserMapper和ArticleMapper,并创建对应的xml文件,在yml文件里配置好
- 工具层(common):统一返回类(ResponseAdvice,AjaxResult等)
注意:创建完相应的类就得将需要加的注解加上去,这里就不一一展示了
创建完后的目录结构
yml配置文件的内容
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
demo: debug
将前端的东西导入static
实现common工具类
由于工具类后面我们基本都会用到,所以这里先实现一下
实现拦截器验证用户登录
- 步骤1创建一个Constant类,定义session的key
- 步骤2:创建一个普通类实现HandlerInterceptor接口,重写preHandle
- 步骤3:创建一个普通类实现 WebMvcConfigurer接口,重写addInterceptors
public class Constant {
//登录信息存储到session中的key值
public static final String SESSION_USERINFO_KEY = "session_userinfo_key";
}
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断登录业务
HttpSession session = request.getSession(false);//会根据请求发送来的sessionId去服务器找对应的会话
if(session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {//根据key拿到value
return true;
}
response.setStatus(401);
return false;
}
}
@Configuration
public class AppConfig implements WebMvcConfigurer {
//不拦截的url
List<String> excludes = new ArrayList<>() {{
add("/**/*.html");
add("/js/**");
add("/editor.md/**");
add("/css/**");
add("/img/**"); // 放行 static/img 下的所有文件
}};
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置拦截器
InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
registration.addPathPatterns("/**");
registration.excludePathPatterns(excludes);
}
}
实现统一数据返回格式
步骤1:创建一个普通的类,实现业务成功返回的方法和业务失败返回的方法
步骤2:创建一个普通的类实现ResponseAdvice接口,重写supports方法和beforeBodyWrite方法
代码
public class AjaxResult {
/**
* 业务执行成功返回的方法
* @param data
* @return
*/
public static HashMap<String,Object> success(Object data) {
HashMap<String,Object> result = new HashMap<>();
result.put("code",200);
result.put("msg","");
result.put("data",data);
return result;
}
/**
* 业务执行成功时返回的代码
* @param data
* @param msg
* @return
*/
public static HashMap<String,Object> success(Object data,String msg) {
HashMap<String,Object> result = new HashMap<>();
result.put("code",200);
result.put("msg",msg);
result.put("data",data);
return result;
}
/**
* 业务执行失败返回的方法
* @param code
* @param data
* @param msg
* @return
*/
public static HashMap<String,Object> fail(int code,Object data,String msg) {
HashMap<String,Object> result = new HashMap<>();
result.put("code",code);
result.put("msg",msg);
result.put("data",data);
return result;
}
/**
* 业务执行失败返回的方法
* @param code
* @param msg
* @return
*/
public static HashMap<String,Object> fail(int code,String msg) {
HashMap<String,Object> result = new HashMap<>();
result.put("code",code);
result.put("msg",msg);
result.put("data","");
return result;
}
}
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof HashMap) {
return body;
}
//如果body是字符串类型,需要特殊处理
if(body instanceof String) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(AjaxResult.success(body));
}
return AjaxResult.success(body);
}
}
实现加盐加密类
创建一个普通类,实现两个方法,encrypt方法和decrypt方法。
- encrypt方法根据用户输入的密码,进行加盐加密,返回最终加密的密码
- decrypt方法根据用户输入的密码和数据库存的加密的密码进行验证
public class SecurityUtil {
/**
* 对password进行加盐加密
* @param password
* @return
*/
public static String encrypt(String password) {
}
/**
* 验证password和数据库拿出来的finalPassword进行验证
* @param password
* @param finalPassword
* @return
*/
public static String decrypt(String password,String finalPassword) {
}
}
实现encrypt方法
加盐思路:用UUID类生成一个32长度的字符串作为盐值,然后将盐值+password进行md5加密生成一个32长度的字符串,然后盐值+这个有md5加密的字符串,就是最终的结果
/**
* 对password进行加盐加密
* @param password
* @return
*/
public static String encrypt(String password) {
//每次生成内容不同,但是长度固定为32的字符串
String salt = UUID.randomUUID().toString().replace("-","");
//盐值+password进行md5加密
String finalPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
//返回盐值+密码,总共64位,存到数据库中
return salt+finalPassword;
}
实现decrypt方法
这个方法用来解密验证密码
思路:这个方法有两个参数password:待验证的密码,finalPassword:最终正确的密码(在数据库查询的密码),从finalPassword中提取出盐值,然后盐值+password进行md5加密生成一个字符串,然后盐值+字符串和finalPassword判断是否相等
/**
* 验证password和数据库拿出来的finalPassword进行验证
* password:待验证的密码
* finalPassword:最终正确的密码(在数据库查询的密码)
* @param password
* @param finalPassword
* @return
*/
public static boolean decrypt(String password,String finalPassword) {
//非空效验
if(!StringUtils.hasLength(password) || !StringUtils.hasLength(finalPassword)) {
return false;
}
if(finalPassword.length()!=64) {
return false;
}
//提取出盐值
String salt = finalPassword.substring(0,32);
//使用盐值+密码生成一个32位的密码
String securityPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
//使用上一个32位的密码拼接上盐值进行密码验证
return (salt+securityPassword).equals(finalPassword);
}
实现SessionUtil类
这个工具类用来查询当前用户登录的session信息
public class SessionUtil {
public static UserInfo getLoginUser(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session!=null && session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {
UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
return userInfo;
}
return null;
}
}
实现注册页面
这里开始就涉及到前后端交互了,要约定好前后端交互的接口
实现前端代码
那先来编写前端代码(打开reg.html进行编写代码)
记得要引入jquery
mysub方法
function mysub() {
//1.非空效验
var username = jQuery("#username");
var password = jQuery("#password");
var password2 = jQuery("#password2");
if(username.val()=="") {
alert("用户名为空");
username.focus();
return false;
}
if(password.val()=="") {
alert("密码为空");
password.focus();
return false;
}
if(password2.val()=="") {
alert("确认密码为空");
password2.focus();
return false;
}
if(password.val()!=password2.val()) {
alert("两次输入的密码不一致");
return false;
}
//2.发送ajax请求
jQuery.ajax({
url:"/user/reg",
type:"POST",
data:{
username:username.val(),
password:password.val()
},
success:function(result) {
if(result.code==200 && result.data==1) {
alert("注册成功");
if(confirm("是否去登录?")) {
location.href="login.html";
}
} else if(result.code==-2 && result.msg!=null) {
alert("注册失败,用户名已存在");
} else {
alert("注册失败请重试");
}
}
});
}
实现后端代码
由前端代码可知,url为/user/reg,我们需要在AppConfig里放行这个url,因为默认是所有url都要拦截,但是注册不能拦截,因为还没注册怎么能登录呢
后端代码基本思路就是,controller调用service,service调用mapper
所以要在UserController中注入UserService,在UserService中注入UserMapper
在UserController中实现reg方法
@RequestMapping("/reg")
public Object reg(String username,String password) {
//1.非空效验
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return AjaxResult.fail(-1,"非法参数");
}
//根据用户名查询用户
UserInfo userInfo = userService.getUserByName(username);
if(userInfo!=null && userInfo.getId()>0) {
//用户名已经存在
return AjaxResult.fail(-2,"用户名已存在");
}
//2.进行添加操作
int result = userService.add(username, SecurityUtil.encrypt(password));
if(result==1) {
return AjaxResult.success(1,"添加成功");
} else {
return AjaxResult.fail(-1,"添加失败");
}
}
可以看到,我们还需要在UserService中实现getUserByName和add方法
public UserInfo getUserByName(String username) {
return userMapper.getUserByName(username);
}
public int add(String username,String password) {
return userMapper.add(username,password);
}
实现完之后,还需要在UserMapper接口中定义getUserByName方法和add方法,然后在对应的xml文件中实现sql语句
UserMapper
public UserInfo getUserByName(@Param("username") String username);
public int add(@Param("username") String username,@Param("password") String password);
对应的UserMapper.xml文件
<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username}
</select>
<insert id="add">
insert into userinfo(username,password)
values(#{username},#{password})
</insert>
到这里注册页面就实现完成了
实现登录页面
实现前端代码
打开login.html进行编写代码
还是一样,引入jquery,然后在submit设置onclick监听,然后实现mysub()方法
function mysub() {
//1.非空效验
var username = jQuery("#username");
var password = jQuery("#password");
if(username.val()=="") {
alert("用户名为空");
username.focus();
return false;
}
if(password.val()=="") {
alert("密码为空");
password.focus();
return false;
}
//2.发送ajax请求
jQuery.ajax({
url:"/user/login",
type:"POST",
data:{
"username":username.val(),
"password":password.val()
},
success:function(result) {
if(result.code==200 && result.data==1) {
location.href="myblog_list.html";
} else {
alert("用户名或密码错误");
username.focus();
}
}
});
}
实现后端代码
先放行/user/login接口
然后在UserController中编写login方法
@RequestMapping("/login")
public int login(HttpServletRequest request,String username,String password) {
//1.非空效验
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return 0;
}
//2.进行数据库查询操作
UserInfo userInfo = userService.getUserByName(username);
if(userInfo!=null && userInfo.getId()>0) {
//用户名正确,验证密码
if(SecurityUtil.decrypt(password,userInfo.getPassword())) {
//将userInfo存到session中
HttpSession session = request.getSession(true);
session.setAttribute(Constant.SESSION_USERINFO_KEY,userInfo);
return 1;
}
}
return 0;
}
实现个人主页
实现退出登录功能
当然由于退出登录功能在很多页面中都应该存在,所以后面可能就不详细说明
实现前端代码
退出登录功能后面很多页面都会用,所以我们新建一个Tool.js文件,将退出登录的前端方法写在里面
然后在Tool.js中编写代码
//退出登录
function onExit() {
if(confirm("是否确认退出?")) {
//发送ajax请求退出
jQuery.ajax({
url:"/user/logout",
type:"POST",
data:{},
success: function(result) {
location.href="login.html";
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
}
编写myblog_list.html文件
并引入Tool.js和jquery
然后只需要修改一句代码即可
当你点击退出登录,它会自动调用Tool.js里的onExit()方法
实现后端代码
当用户点击退出登录,发送ajax请求到后端时,后端就将用户对应的session给删除即可。
在UserController中编写代码
@RequestMapping("/logout")
public boolean logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session!=null && session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {
session.removeAttribute(Constant.SESSION_USERINFO_KEY);
}
return true;
}
初始化个人信息(包括个人文章列表的个人信息)和完成删除功能
当跳转到myblog_list.html时,就前端就调用initList()和myinfo()方法,这两个方法发送ajax请求去查询数据库,然后将个人信息和个人发表的文章返回给前端,前端再根据数据进行渲染
实现前端代码
<script>
var descLen = 60;//简介最大长度
//这个方法用来从正文中提取字符串
function mySubStr(content) {
if(content.length>desLen) {
return content.substr(0,descLen);
}
return content;
}
//初始化个人列表信息
function initList() {
jQuery.ajax({
url:"/art/mylist",
type:"POST",
data:{},//不用传uid,因为session中有userinfo,不能轻信前端传来的参数
success:function(result) {
if(result.code==200 && result.data!=null && result.data.length>0) {
var html = "";
for(var i=0;i<result.data.length;i++) {
var item = result.data[i];
//如果state==2,说明是草稿箱里的文章,不显示出来
if(item.state==2) {
continue;
}
html+='<div class="blog">';
html+='<div class="title">'+item.title+'</div>';
html+='<div class="date">'+item.createtime+'</div>'+'<div class="desc">'+mySubstr(item.content)+' </div>';
html+='<div style="text-align: center;margin-top: 50px;">';
html+='<a href="blog_content.html?id='+item.id+'">查看详情</a> ';
html+='<a href="blog_update.html?id='+item.id+'">修改</a> <a href="javascript:myDel('+item.id+')">删除</a></div>';
html+='</div>';
}
jQuery("#artlistDiv").html(html);
} else {
//此人没有发表文章
jQuery("#artlistDiv").html("<h1>暂无数据</h1>");
}
},
error:function(err) {
if(err!=null && err.statue==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
initList();//当浏览器渲染引擎执行到此行时就会调用此方法
//获取个人信息
function myinfo() {
jQuery.ajax({
url:"user/myinfo",
type:"POST",
data:{},
success:function(result) {
if(result.code==200 && result.data!=null) {
jQuery("#username").text(result.data.username);
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
myinfo();
//删除功能
function myDel(aid) {
jQuery.ajax({
url:"art/del",
type:"POST",
data:{
"aid":aid
},
success:function(result) {
if(result.code==200 && result.data==1) {
alert("删除成功");
location.href="myblog_list.html";
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录");
location.href="login.html"
} else {
alert("删除失败");
location.href="myblog_list.html"
}
}
});
}
</script>
代码很长,但其实主要也就三个主要的方法,这3个方法都有ajax请求,所以需要在后端进行处理,然后返回结果
实现后端代码
先处理initList的ajax请求
对于initList发送来的ajax请求,我们要根据sesion里存的userInfo的id去查文章表拿到该用户的所有文章信息,然后进行返回
同样,先进行注入
@RequestMapping("/mylist")
public List<ArticleInfo> myList(HttpServletRequest request) {
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null) {
return articleService.getMyList(userInfo.getId());
}
return null;
}
接下来一样要在ArticleService中实现getMyList方法
在ArticleService中也要先注入
public List<ArticleInfo> getMyList(Integer uid) {
return articleMapper.getMyList(uid);
}
接下来在ArticleMapper中定义getMyList方法,然后在对应的xml文件中编写sql语句
public List<ArticleInfo> getMyList(@Param("uid") Integer uid);
<select id="getMyList" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo where uid=#{uid}
</select>
接下来处理myinfo的ajax请求
@RequestMapping("/myinfo")
public UserInfo myInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session!=null && session.getAttribute(Constant.SESSION_USERINFO_KEY)!=null) {
UserInfo userInfo = SessionUtil.getLoginUser(request);
return userInfo;
}
return null;
}
接下来处理myDel发送来的ajax请求
@RequestMapping("/del")
public Integer artDel(HttpServletRequest request,Integer aid) {
//非空效验
if(aid!=null && aid>0) {
//根据文章id查询文章详情
ArticleInfo articleInfo = articleService.getDetail(aid);
//进行归属人验证
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(articleInfo!=null && userInfo!=null && userInfo.getId()!=articleInfo.getUid()) {
//归属人正确进行删除
int result = articleService.artDel(aid);
return result;
}
}
return null;
}
接下来得在ArticleService中实现getDetail方法和artDel方法
public ArticleInfo getDetail(Integer aid) {
return articleMapper.getDetail(aid);
}
public Integer artDel(Integer aid) {
return articleMapper.artDel(aid);
}
接下来在ArticleMapper中定义getDetail方法和artDel方法,然后在对应的xml文件中编写sql语句
public ArticleInfo getDetail(@Param("aid") Integer aid);
public Integer artDel(@Param("aid") Integer aid);
<select id="getDetail" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo where id=#{aid}
</select>
<delete id="artDel">
delete from articleinfo where id=#{aid}
</delete>
实现详情页(blog_content.html)
myblog_list.html的页面效果大概是这样的,其中删除功能已经实现了,那么现在来实现查看详情功能,当点击查看详情时,就会跳转blog_content.html页面,并将文章id传过去(如下图)
实现前端代码
那接下来打开blog_content.html来进行编写前端代码
这个页面同样也有退出登录功能,跟上面写法一样
在编写之前,我们需要在Tool.js中写一个方法getURLParam,这个方法用来从url上获取参数
这里直接给代码
// 获取当前 url 中某个参数的方法
function getURLParam(key){
var params = location.search;
if(params.indexOf("?")>=0){
params = params.substring(1);
var paramArr = params.split('&');
for(var i=0;i<paramArr.length;i++){
var namevalues = paramArr[i].split("=");
if(namevalues[0]==key){
return namevalues[1];
}
}
}else{
return "";
}
}
然后开始编写blog_content.html
//获取文章详细信息
function getArtDetail() {
//从url中获取文章id,也就是在myblog_list.html跳转到这里时url中的参数
var aid = getURLParam("id");
if(aid!=null && aid>0) {
//发送ajax请求,查询数据库拿到文章详情
jQuery.ajax({
url:"art/detail",
type:"POST",
data:{"aid":aid},
success:function(result) {
if(result.code==200 && result.data!=null) {
var art = result.data;
jQuery("#title").text(art.title);
jQuery("#date").text(art.createtime);
jQuery("#rcount").text(art.rcount);
editormd = editormd.markdownToHTML("editorDiv",{
markdown : art.content
});
//根据uid获取个人信息
myInfo(art.uid);
}
}
});
}
}
getArtDetail();
//根据uid获取个人信息
function myInfo(uid) {
jQuery.ajax({
url:"user/myinfobyid",
type:"POST",
data:{"uid":uid},
success:function(result) {
if(result.code==200 && result.data!=null) {
jQuery("#username").text(result.data.username);
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户为登录,即将跳转登录页面");
location.href="login.html"
}
}
});
}
实现后端代码
上面前端代码也是两个ajax请求
先来处理getArtDetail方法发送来的ajax请求
要在AppConfig中放行detail接口
@RequestMapping("/detail")
public Object getDetail(Integer aid) {
if(aid!=null && aid>0) {
ArticleInfo articleInfo = articleService.getDetail(aid);
//访问量加1
synchronized (locker) {
int result = articleService.rountAdd(aid,articleInfo.getContent()+1);
if(result!=1) {
return AjaxResult.fail(-1,"访问量添加失败");
}
}
//返回文章详情
return AjaxResult.success(articleInfo);
}
return AjaxResult.fail(-1,"查询失败");
}
接下来要在ArtclieService中实现rcountAdd方法
public int rcountAdd(Integer aid,Integer rcount) {
return articleMapper.rcountAdd(aid,rcount);
}
接下来在ArticleMapper中定义rcountAdd方法并在对应的xml文件中编写sql语句
public int rcountAdd(@Param("aid") Integer aid,@Param("rcount") Integer rcount);
<update id="rcountAdd">
update articleinfo set rcount=#{rcount} where id=#{aid}
</update>
然后处理myinfo发送的ajax请求,要在AppConfig放行/user/myinfobyuid
@RequestMapping("/myinfobyuid")
public UserInfo getMyInfoByUid(Integer uid) {
if(uid!=null && uid>0) {
return userService.getMyInfoByUid(uid);
}
return null;
}
public UserInfo getMyInfoByUid(Integer uid) {
return userMapper.getMyInfoByUid(uid);
}
public UserInfo getMyInfoByUid(@Param("uid") Integer uid);
<select id="getMyInfoByUid" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id=#{uid}
</select>
实现博客修改功能(blog_update.html)
当点击修改时,就会将跳转到blog_update.html并将id传过去
实现前端代码
同样这个页面有退出登录功能,实现方法跟上面一样
function mysub(){
// alert(editor.getValue()); // 获取值
// editor.setValue("#123") // 设置值\
var title = jQuery("#title");
var content = editor.getValue();
//非空效验
if(title=="") {
title.focus();
alert("请先输入标题");
return false;
}
if(content=="") {
alert("请输入正文");
return false;
}
jQuery.ajax({
url:"/art/update",
type:"POST",
data:{
"aid":aid,
"title":title.val(),
"content":content
},
success:function(result) {
if(result.code==200 && result.data>0) {
alert("修改成功");
location.href="myblog_list.html";
} else {
alert("修改失败,请重试");
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录");
location.href="login.html";
}
}
});
}
//查询文章详情并展现
function showArt() {
//从url中获取文章id
aid = getURLParam("id");
if(aid!=null && aid>0) {
//访问后端详情
jQuery.ajax({
url:"/art/detailbyid",
type:"POST",
data:{"aid":aid},
success:function(result) {
if(result.code==200 && result.data!=null) {
var art = result.data;
jQuery("#title").val(art.title);
initEdit(art.content);
} else {
alert("您没有权限修改");
location.href="myblog_list.html";
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户还未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
}
showArt();
实现后端代码
上面前端代码中涉及到两个ajax请求
先来处理showArt方法发送的ajax请求
@RequestMapping("/detailbyid")
public Object getDetailById(HttpServletRequest request,Integer aid) {
if(aid!=null && aid>0) {
//根据文章id查询文章详情
ArticleInfo articleInfo = articleService.getDetail(aid);
//文章归属人验证
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(articleInfo!=null && userInfo!=null && articleInfo.getUid()==userInfo.getId()) {
//文章归属人正确
return AjaxResult.success(articleInfo);
}
}
return AjaxResult.fail(-1,"查询失败");
}
接下来处理mysub发送的ajax请求
@RequestMapping("/update")
public int update(HttpServletRequest request,Integer aid,String title,String content) {
//非空效验
if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content) || aid == 0 || aid<=0) {
return 0;
}
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null && userInfo.getId()>0) {
return articleService.update(aid,userInfo.getId(),title,content);
}
return 0;
}
然后在ArticleService中实现update
public int update(Integer aid,Integer uid,String title,String content) {
return articleMapper.update(aid,uid,title,content);
}
然后在ArticleMapper中定义update并在对应的xml文件中编写sql语句
public int update(@Param("aid") Integer aid,@Param("uid") Integer uid,
@Param("title") String title,@Param("content") String content);
<update id="update">
update articleinfo set title=#{title},content=#{content}
where id=#{aid} and uid=#{uid}
</update>
实现博客编辑页
实现前端代码
function mysub(){
// alert(editor.getValue()); // 获取值
// editor.setValue("#123") // 设置值
var title = jQuery("#title");
var content = editor.getValue();
//非空效验
if(title=="") {
title.focus();
alert("请先输入标题");
return false;
}
if(content=="") {
alert("请先输入正文");
return false;
}
jQuery.ajax({
url:"/art/release",
type:"POST",
data:{
"title":title.val(),
"content":content
},
success:function(result) {
if(result.code==200 && result.data>0) {
alert("发布成功");
location.href="myblog_list.html";
} else {
alert("发布失败,请重试");
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
//检查是否已经登录
function checkIsLogged() {
jQuery.ajax({
url:"/user/islogged",
type:"GET",
success:function(result) {
return true;
},
error:function(err) {
alert("用户为登录,即将跳转登录页面");
location.href="login.html";
}
});
}
checkIsLogged();
//保存文章到草稿箱,将state设置为2
function draft() {
var title = jQuery("#title");
var content = editor.getValue();
//非空效验
if(title=="") {
title.focus();
alert("请先输入标题");
return false;
}
if(content=="") {
alert("请先输入正文");
return false;
}
jQuery.ajax({
url:"/art/draft",
type:"POST",
data:{
"title":title.val(),
"content":content
},
success:function(result) {
if(result.code==200 && result.data>0) {
alert("保存成功");
location.href="myblog_list.html";
} else {
alert("发布失败,请重试");
}
},
error:function(err) {
if(err!=null && err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
实现后端代码
上面的前端代码总共涉及到3个ajax请求
先来处理 checkIsLogged方法的ajax请求,这个主要用来验证时候登录
@RequestMapping("/islogged")
public int isLogged(HttpServletRequest request, HttpServletResponse response) {
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null) {
return 1;
}
response.setStatus(401);
return 0;
}
然后处理mysub()方法的ajax请求,这个方法用来发布文章,默认state1说明是发布的文章,state2是保存在草稿箱的文章
@RequestMapping("/release")
public int releaseArt(HttpServletRequest request,String title,String content) {
//非空效验
if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
return 0;
}
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null && userInfo.getId()>0) {
int result = articleService.releaseArt(title,content,userInfo.getId());
if(result==1) {
return 1;
}
}
return 0;
}
还要在ArtcileService中实现releaseArt方法,然后在ArticleMapper中定义releaseArt方法,并在对应的xml文件中编写sql语句
@RequestMapping("/release")
public int releaseArt(HttpServletRequest request,String title,String content) {
//非空效验
if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
return 0;
}
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null && userInfo.getId()>0) {
int result = articleService.releaseArt(title,content,userInfo.getId());
if(result==1) {
return 1;
}
}
return 0;
}
public int releaseArt(@Param("title")String title,@Param("content") String content,@Param("uid") Integer uid);
<insert id="releaseArt">
insert into articleinfo (title,content,uid) values (#{title},#{content},#{uid})
</insert>
最后处理保存文章也就是draft()的ajax请求,处理方法是将文章保存到数据库中,但是要将字段state设置为2,然后前端在渲染时,如果state==2,文章会显示在我的草稿箱中
@RequestMapping("/draft")
public int draftArt(HttpServletRequest request,String title,String content) {
//非空效验
if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
return 0;
}
int state = 2;//将state设置为2
UserInfo userInfo = SessionUtil.getLoginUser(request);
if(userInfo!=null && userInfo.getId()>0) {
//将文章保存到数据库,并将state设置为2,表示还未发布
int result = articleService.draftArt(title,content,userInfo.getId(),2);
return result;
}
return 0;
}
还是老步骤在,ArticleService中实现draftArt,然后在ArticleMapper定义draftArt方法,并在对应的xml文件中编写sql语句
public int draftArt(String title,String content,Integer uid,Integer state) {
return articleMapper.draftArt(title,content,uid,state);
}
public int draftArt(@Param("title") String title,@Param("content") String content,
@Param("uid") Integer uid,@Param("state") Integer state);
<insert id="draftArt">
insert into articleinfo (title,content,uid,state) values (#{title},#{content},#{uid},#{state})
</insert>
实现我的草稿箱(draft_list.html)
当点击我的草稿箱时,就会跳转到draft_list.html页面
实现前端代码
draft_list.html的代码基本和myblog_list.html的代码一样,最主要的区别主要是state2还是state2
var descLen = 60;//简介最大长度
//字符串截取,将文章正文截取成简介
function mySubstr(content) {
if(content.length>descLen) {
return content.substr(0,descLen);
}
return content;
}
//初始化个人列表信息
function initList() {
jQuery.ajax({
url:"/art/mylist",
type:"POST",
data:{},//不用传uid,因为session中有userinfo,不能轻信前端的参数
success:function(result) {
if(result.code==200 && result.data!=null && result.data.length>0) {
//todo:有文章
var html="";
var count = 0;
for(var i=0;i<result.data.length;i++) {
var item=result.data[i];
//如果state==2说明是草稿箱里的文章,显示出来
if(item.state==2) {
html+='<div class="blog">';
html+='<div class="title">'+item.title+'</div>';
html+='<div class="date">'+item.createtime+'</div>'+'<div class="desc">'+mySubstr(item.content)+' </div>';
html+='<div style="text-align: center;margin-top: 50px;">';
html+='<a href="javascript:publish('+item.id+')">发布文章</a> ';
html+='<a href="blog_update.html?id='+item.id+'">修改</a> <a href="javascript:myDel('+item.id+')">删除</a></div>';
html+='</div>';
count++;
}
}
jQuery("#artlistDiv").html(html);
if(count==0) {
//此人草稿箱没有文章
jQuery("#artlistDiv").html("<h1>暂无数据</h1>");
}
}
},
error:function(err) {
if(err!=null&&err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
initList();//当浏览器渲染引擎执行到此行时就会调用此方法
//获取个人信息
function myInfo() {
jQuery.ajax({
url:"/user/myinfo",
type:"POST",
data:{},
success:function(result) {
if(result.code==200 && result.data!=null) {
jQuery("#username").text(result.data.username);
}
},
error:function(err) {
if(err!=null&&err.status==401) {
alert("用户未登录,即将跳转登录页面");
location.href="login.html";
}
}
});
}
myInfo();
//删除功能
function myDel(aid) {
jQuery.ajax({
url:"/art/del",
type:"POST",
data:{
"aid":aid
},
success:function(result) {
if(result.code==200 && result.data==1) {
alert("删除成功");
location.href="draft_list.html";
}
},
error:function(err) {
if(err!=null&&err.status==401) {
alert("该用户没有删除权限!");
} else {
alert("删除失败!");
}
location.href="draft_list.html";
}
});
}
//将草稿箱里的文章发布:将state设置成1
function publish(aid) {
jQuery.ajax({
url:"/art/publish",
type:"POST",
data:{
"aid":aid
},
success:function(result) {
if(result.code==200 && result.data==1) {
alert("发布成功");
location.href="myblog_list.html";
}
},
error:function(err) {
if(err!=null&&err.status==401) {
alert("该用户没有发布权限!");
} else {
alert("发布失败!");
}
location.href="draft_list.html";
}
});
}
实现后端代码
上面涉及到的3个ajax请求,前两个ajax请求在之前的后端代码已经处理了,所以这里只需要再处理publish方法里的ajax请求即可
@RequestMapping("/publish")
public int publishArt(Integer aid) {
if(aid!=null) {
//将state设置为1
int result = articleService.publishArt(aid,1);
if(result==1) {
return 1;
}
}
return 0;
}
然后在ArticleService中实现draftArt方法,接着在ArticleMapper中定义draftArt方法并在对应的xml文件中编写sql语句
public int publishArt(Integer aid,Integer state) {
return articleMapper.publishArt(aid,state);
}
public int publishArt(@Param("aid") Integer aid,@Param("state") Integer state);
<update id="publishArt">
update articleinfo set state=#{state} where id=#{aid}
</update>
实现博客主页(blog_list.html)
博客主页存放所有的博客,有5个按钮分别是,查看全文,首页,上一页,下一页和末页
首页,上一页,下一页和末页就属于分页功能了,接下来讲一下分页的思路
分页要素:
- 页码(pageIndex):要查询第几页的数据
- 每页显示多少条数据(pageSize):每页展示最大长度数据
首先所有的数据都是从数据库中去查询的,查询的语句中有一个关键字limit,这样就可以限制发送多少条记录去前端,比如limit pageSize ,就查询pageSize条记录,然后发送去前端然后进行渲染页面
数据库还有一个关键字就是offset(偏移量),比如说limit 2 offer 2,就是跳过前两条记录,然后查询第3,第4条记录。
那么比如你想查询第pageIndex页的数据,那么它的偏移量就是pageSize*(pageIndex-1)
所以分页公式(偏移量):pageSize*(pageIndex-1)
分页语法:select * from articleinfo limit pageSize offset pageSize * (pageIndex-1)
实现前端代码
var descLen = 200;//简介最大长度
//字符串截取,将文章正文截取成简介
function mySubstr(content) {
if(content.length>descLen) {
return content.substr(0,descLen);
}
return content;
}
var pindex = 1;//当前的页码
var psize = 2;//每页显示的条数信息
var totalpage = 1;//总页数
//初始化分页参数,尝试从url中获取pindex和psize
function initPageParam() {
var pi = getURLParam("pindex");
if(pi!="") {
pindex=pi;
}
var ps = getURLParam("psize");
if(ps!="") {
psize=ps;
}
}
initPageParam();
//查询分页数据
function getList() {
jQuery.ajax({
url:"/art/list",
type:"GET",
data:{
"pindex":pindex,
"psize":psize
},
success:function(result) {
if(result.code==200 && result.data!=null && result.data.length>0) {
//循环拼接控制document
var finalHtml = "";
for(var i=0;i<result.data.length;i++) {
var item = result.data[i];
//如果state==2说明是草稿箱的文章,不显示出来
if(item.state==2) {
continue;
}
finalHtml+='<div class="blog">';
finalHtml+='<div class="title">'+item.title+'</div>';
finalHtml+='<div class="date">'+item.createtime+'</div>';
finalHtml+='<div class="desc">'+mySubstr(item.content)+'</div>';
finalHtml+='<a href="blog_content.html?id='+item.id+'" class="detail">查看全文</a>';
finalHtml+='</div>';
}
jQuery("#listDiv").html(finalHtml);
}
}
});
}
getList();
//查询总共多少页
function getTotalPage() {
jQuery.ajax({
url:"/art/totalpage",
type:"GET",
data:{
"psize":psize
},
success:function(result) {
if(result.code==200 && result.data!=null) {
totalpage=result.data;
}
}
});
}
getTotalPage();
//首页
function firstClick() {
location.href="blog_list.html";
}
//上一页
function beforeClick() {
if(pindex<=1) {
alert("前面已经没有内容了!");
return false;
}
pindex-=1;
location.href="blog_list.html?pindex="+pindex+"&psize="+psize;
}
//下一页
function nextClick() {
pindex = parseInt(pindex);
if(pindex>=totalpage) {
//已经是最后一页
alert("后面已经没有内容了哦!");
return false;
}
pindex+=1;
location.href="blog_list.html?pindex="+pindex+"&psize="+psize;
}
//末页
function lastClick() {
pindex=totalpage;
location.href="blog_list.html?pindex="+pindex+"&psize="+psize;
}
实现后端代码
上面涉及到两个ajax请求
两个请求的url都需要放行
先来处理getTotalPage方法的ajax请求
@RequestMapping("/totalpage")
public Integer getTotalCount(Integer psize) {
if(psize!=null) {
//参数有效
int totalCount = articleService.getTotalCount();
int totalPage = (int)Math.ceil(totalCount*1.0/psize);
return totalPage;
}
return null;
}
接下来在ArticleService中实现getTotalCount方法,然后在ArticleMapper中定义getTotalCount方法并在对应的xml文件中编写sql语句
public int getTotalCount() {
return articleMapper.getTotalCount();
}
public int getTotalCount();
<select id="getTotalCount" resultType="java.lang.Integer">
select count(*) from articleinfo where state=1
</select>
接下来处理getList的ajax请求
public List<ArticleInfo> getList(Integer psize,Integer offset) {
return articleMapper.getList(psize,offset);
}
然后在ArticleService中实现getList,还要在ArticleMapper中定义getList并在对应的xml文件中编写sql语句
public List<ArticleInfo> getList(@Param("psize") Integer psize,@Param("offset") Integer offset);
<select id="getList" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo limit #{psize} offset #{offset}
</select>
将session持久化到redis
首先你需要在你的linux环境上安装redis,这里就不介绍怎么安装了
然后在你的项目上的application.properties上配置连接redis即可,springboot已经内置了将session持久化到redis,所以你只需要在application.properties上配置即可
spring.redis.host=#你redis安装在哪台机器的ip地址
spring.redis.password=#redis的密码
spring.redis.port=6379#redis默认端口号
spring.redis.database=0#redis操作的数据库
spring.session.store-type=redis
server.servlet.session.timeout=1800#session存到redis的过期时间
spring.session.redis.flush-mode=on_save#会将session保存到redis本地
spring.session.redis.namespace=spring:session
其他扩展功能
- 定时发布功能
- 更换头像
- 冻结账号
等等,大家可以尝试自己实现,下面这个源代码链接,后续有新功能也会将源代码更新
源码链接:https://gitee.com/maze-white/project/tree/master/blog_system_ssm