源码Gitee 地址 SSM博客系统: 基于 SSM 的博客系统 (gitee.com)
数据库设计
drop database myblogsystem if exists myblogsystem;
create database myblogsystem default character set utf8mb4;
use myblogsystem;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(64) not null,
photo varchar(500),
createtime datetime default now(),
updatetime datetime default now(),
state int default 1
) default charset 'utf8mb4';
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,
rcount int not null default 1,
state int default 1,
foreign key(uid) references userinfo(id)
) default charset 'utf8mb4';
用户表跟文章表,一个用户有多个文章,一篇文章属于一个用户,属于一对多关系
配置文件说明
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/myblogsystem?characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置静态资源路径
resources:
static-locations:
- classpath:/static/
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration: #配置打印 Mybatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
demo: debug
# 配置图片存放本地路径
HEADPHOTO_SAVE_PATH:
D:/Gitee/ssm-blog-system/myblogsystem/src/main/resources/static/img
# 配置端口号
server:
port: 9090
统一登录验证
1、创建 配置类 AppConfig 实现 WebMvcConfigurer 接口,重写addInterceptors 方法,添加需要拦截的 接口
2、创建组件类 LoginInterceptor 实现 HandlerInterceptor接口 重写 preHandle方法,编写具体拦截规则
package com.example.demo.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
List<String> list = new ArrayList<String>() {{
add("/login.html");
add("/css/**");
add("/editor.md/**");
add("/img/**");
add("/js/**");
add("/user/hello");
add("/user/login");
add("/user/reg");
add("/reg.html");
add("/user/getphoto");
add("/1-HeadPhoto.jpg");
}};
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
registration.addPathPatterns("/**");
registration.excludePathPatterns(list);
}
}
package com.example.demo.common;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
return true;
}
response.setStatus(401);
return false;
}
}
统一数据返回
1、创建一个被@ControllerAdvice修饰的类,并且实现 ResponseBodyAdvice接口,重写 supports 方法跟 beforeBodyWrite 方法
package com.example.demo.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
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 String) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(AjaxResult.success("", body));
}
if(body instanceof HashMap) {
return body;
}
return AjaxResult.fail(-1, "未知原因");
}
}
后端接口
登录接口
采用 MD5 加盐算法,实现密码加密
后端代码
@RequestMapping("/login")
public Object login(HttpServletRequest request, String username, String password) {
//用户名或密码为空
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return AjaxResult.fail(-1, "参数非法");
}
UserInfo userinfo = userService.getUserInfoByUsername(username);
//判断用户名密码是否正确
if(!SecurityUtil.decrypt(password, userinfo.getPassword())) {
return AjaxResult.fail(-1, "用户名或密码错误");
}
HttpSession session = request.getSession(true);
session.setAttribute(Constant.SESSION_USERINFO_KEY, userinfo);
return AjaxResult.success("用户名密码验证通过", 1);
}
前端 js 代码
<script>
function login() {
var username = document.getElementById("username");
var password = document.getElementById("password");
if(username.value == "" || password.value == "") {
alert("请输入完整");
}
jQuery.ajax({
url:"/user/login",
type:"post",
data:{
"username":username.value,
"password":password.value,
},
success:function(result) {
if(result.data == "1") {
location.href = "myblog_list.html";
} else {
alert("用户名或者密码错误请重试");
}
}
});
}
</script>
注册接口
后端代码
@RequestMapping("/reg")
public Object reg(String username, String password) {
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return AjaxResult.fail(-1, "参数非法");
}
String finalPassword = SecurityUtil.encryot(password);
int result = userService.add(username, finalPassword);
if(result == 1) {
return AjaxResult.success("添加成功", 1);
} else {
return AjaxResult.fail(-1, "数据库添加出错");
}
}
前端 js 代码
<script>
function reg() {
var username = document.getElementById("username");
var password1 = document.getElementById("password1");
var password2 = document.getElementById("password2");
if(username.value == '' || password1.value == '' || password2.value == '') {
alert("请输入完整");
return false;
}
if(password1.value != password2.value) {
alert("两次输入密码不一致");
return false;
}
jQuery.ajax({
url:"/user/reg",
type:"post",
data:{
"username":username.value,
"password":password1.value
},
success:function(result) {
if(result.code == 200 && result.data == 1) {
location.href = "/login.html";
} else {
alert("当前注册人数过多,请稍后再试");
}
}
});
}
</script>
修改用户头像接口
后端代码
@RequestMapping("/changephoto")
public Object changePhoto(@RequestPart("file") MultipartFile headphoto, HttpServletRequest request) throws IOException {
// 拿到图片的类型后缀
String type = headphoto.getOriginalFilename().substring(headphoto.getOriginalFilename().lastIndexOf("."));
HttpSession session = request.getSession();
UserInfo user = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
String path = HEADPHOTO_SAVE_PATH + "/"+ user.getUsername() + type;
String url = "http://localhost:9090/" + "img/"+user.getUsername() + type;
System.out.println("文件本地保存路径 " + path);
System.out.println("远程文件访问地址"+url);
// 更新数据库中的 photo
int ret = userService.updatePhoto(url, user.getId());
if(ret == 1) {
headphoto.transferTo(new File(path));
return AjaxResult.success("修改成功", userService.getUrlbyid(user.getId()));
}
return AjaxResult.fail(-1, "修改失败");
}
前端 js 代码
<script>
document.getElementById('change_avatar').addEventListener('click', function() {
// 当点击更换头像链接时,触发文件输入框的点击事件
document.getElementById('file_input').click();
});
document.getElementById('file_input').addEventListener('change', function() {
var fileInput = $('#file_input');
var formData = new FormData();
formData.append('file', fileInput[0].files[0]);
jQuery.ajax({
type:"post",
url:"/user/changephoto",
data: formData,
processData: false, // 不处理数据
contentType: false, // 不设置内容类型
success:function(result) {
console.log(result.data);
if(result.code == 200)
jQuery("#headphoto").attr("src", result.data + "?tempid=" + Math.random());
console.log(jQuery("#headphoto").attr("src"));
}
});
});
</script>
获取个人信息接口
后端代码
@RequestMapping("/getmyinfo")
public Object getMyinfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
UserInfo result = userService.getMyinfo(userInfo.getId());
return AjaxResult.success(result);
}
前端 js 代码
//获取个人信息
function getmyinfo() {
jQuery.ajax({
url:'user/getmyinfo',
type:'post',
data:{},
success:function(result) {
jQuery("#username").text(result.data.username);
jQuery("#articlecount").text(result.data.articlecount);
jQuery("#headphoto").attr("src", result.data.photo);
console.log(result.data);
console.log(jQuery("#headphoto").attr("src"));
}
});
}
获取个人文章列表接口
后端代码
@RequestMapping("/getmylist")
public Object getmylist(HttpServletRequest request) {
HttpSession session = request.getSession(false);
UserInfo userInfo = (UserInfo)session.getAttribute(Constant.SESSION_USERINFO_KEY);
List<ArticleInfo> result = articleService.getMylist(userInfo.getId());
for(ArticleInfo articleinfo : result) {
String content = articleinfo.getContent();
content = MarkdownToText.markdownToHTML(content);
content = MarkdownToText.HTMLToText(content);
articleinfo.setContent(content);
}
return AjaxResult.success(result);
}
前端 js 代码
//获取博客列表
function getmylist() {
jQuery.ajax({
url:"art/getmylist",
type:"post",
data:{},
success:function(result) {
//博客不为空
if(result.code == 200 && result.data != null && result.data.length > 0) {
var bloglist = "";
result.data.forEach(function(item){
bloglist += '<div class="blog">';
//标题
bloglist += '<div class="title">'+ item.title +'</div>';
//时间
bloglist += '<div class="date">'+ item.updatetime +'</div>';
//正文
//var content = editormd.markdownToHTML(item.content);
bloglist += '<div class="desc">'+ mysubstr(item.content) +'</div>';
//查看详情
bloglist += '<a href="blog_content.html?id='+ item.id + '" class="detail">查看全文</a>';
//修改
bloglist += '<a href="blog_update.html?id='+ item.id + '" class="detail">修改</a>';
//删除
bloglist += '<a href="javascript:mydel('+ item.id+')" class="detail">删除</a>';
//
bloglist += '</div>';
});
jQuery("#mybloglist").html(bloglist);
} else {
jQuery("#mybloglist").html('<h1 align="center">尚未发表博客</h1>');
}
}
});
}
获取文章详情接口
后端代码
@RequestMapping("/getarticledetail")
public Object getArticleDetail(Integer id) {
if(id == null) {
return AjaxResult.fail(-1, "文章ID错误");
}
ArticleInfo result = articleService.getArticleDetail(id);
return AjaxResult.success(result);
}
前端 js 代码
function getArticledetail() {
var aid = getURLParameter("id");
if(aid != null) {
jQuery.ajax({
url:"/art/getarticledetail",
type:"post",
data:{"id":aid},
success:function(result) {
if(result.code == 200) {
var title = jQuery("#title");
title.text(result.data.title);
var date = jQuery("#date");
date.text(result.data.updatetime);
editormd = editormd.markdownToHTML("editorDiv", {
markdown : result.data.content
});
getinfobyid(result.data.uid);
}
}
});
}
}
获取所有用户文章列表
后端代码
@RequestMapping("/getlist")
public Object getList(Integer pageIndex, Integer pageSize) {
if(pageIndex == null || pageSize == null) {
return AjaxResult.fail(-1, "资源请求出错");
}
//计算偏移量
int offset = pageSize * (pageIndex - 1);
List<ArticleInfo> result = articleService.getList(offset, pageSize);
for(ArticleInfo articleinfo : result) {
String content = articleinfo.getContent();
content = MarkdownToText.markdownToHTML(content);
content = MarkdownToText.HTMLToText(content);
articleinfo.setContent(content);
}
return AjaxResult.success("获取列表成功", result);
}
前端 js 代码
//初始化所有人的博客
function getList() {
jQuery.ajax({
url:"/art/getlist",
type:"post",
data:{
"pageIndex": pageIndex,
"pageSize": pageSize
},
success:function(result) {
if(result.code == 200 && result.data != "" && result.data != null) {
var bloglist = "";
result.data.forEach(function(item) {
bloglist += '<div class="blog">';
bloglist += '<div class="title" id="title">' + item.title +' </div>';
bloglist += '<div class="date" id="date">' + item.updatetime + '</div>';
bloglist += '<div class="desc" id="content"> '+ mysubstr(item.content)+'</div>';
bloglist += '<a href="blog_content.html?id='+ item.id +'" class="detail">查看全文</a>';
bloglist += '</div>';
});
jQuery("#article").html(bloglist);
} else {
jQuery("#article").html("<h1 align=center>暂无数据</h1>");
}
}
});
}
删除文章接口
后端代码
@RequestMapping("/mydel")
public Object myDel(Integer id) {
int result = articleService.myDel(id);
if(result == 1) {
return AjaxResult.success("删除成功","");
}
return AjaxResult.fail(-1, "删除失败");
}
前端 js 代码
//删除博客
function mydel(id) {
if(confirm("是否确认删除")) {
jQuery.ajax({
url:"/art/mydel",
type:"post",
data:{"id":id},
success:function(result) {
if(result.code == 200) {
location.href = "myblog_list.html";
}
}
});
}
}
退出登录接口
后端代码
@RequestMapping("/logout")
public Object logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
session.invalidate();
return AjaxResult.success("退出成功", 1);
}
前端 js 代码
//退出登录
function logout() {
if(confirm("是否确认退出")){
jQuery.ajax({
url:"/user/logout",
type:"post",
data:{},
success:function(result){
if(result.data == 1) {
location.href = "login.html";
}
},
error:function() {
alert("当前未登录,即将跳转到登录页面");
location.href = "/login.html";
}
});
}
}
上传文章接口
后端代码
@RequestMapping("/mysub")
public Object mySub(HttpServletRequest request, String title, String content) {
HttpSession session = request.getSession(false);
UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
int result = articleService.mySub(userInfo.getId(), title, content);
if(result == 1) {
return AjaxResult.success("提交成功", 1);
} else {
return AjaxResult.fail(-1, "提交失败");
}
}
前端 js 代码
// 提交
function mysub(){
// alert(editor.getValue()); // 获取值
// editor.setValue("#123") // 设置值
var title = jQuery("#title");
if(title.val() == "") {
alert("请先输入值");
return;
}
var content = editor.getValue();
if(content == "") {
alert("请先输入正文");
return;
}
jQuery.ajax({
url:"/art/mysub",
type:"post",
data:{
"title": title.val(),
"content": content
},
success:function(result) {
if(result.code == 200) {
alert("提交成功");
location.href = "myblog_list.html";
} else {
alert("提交失败,请稍后再试");
}
}
});
}
文章修改接口
文章修改功能的实现流程是先将文章内容从后端取得后,展示在前端,用户修改完成之后进行重新提交 所以依赖于前面的获取文章详情接口
后端代码
@RequestMapping("/update")
public Object myUpdate(HttpServletRequest request, Integer aid, String title, String content) {
HttpSession session = request.getSession(false);
UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
int result = articleService.myUpdate(userInfo.getId(), aid, title, content);
if(result == 1) {
return AjaxResult.success("修改成功", 1);
} else {
return AjaxResult.fail(-1, "修改失败");
}
}
前端 js 代码
//获取文章详细信息
function getArticledetail() {
aid = getURLParameter("id");
if(aid != null) {
jQuery.ajax({
url:"/art/getarticledetail",
type:"post",
data:{"id":aid},
success:function(result) {
if(result.code == 200) {
var title = jQuery("#title");
title.val(result.data.title);
initEdit(result.data.content);
}
}
});
}
}
// 提交
function mysub(){
// alert(editor.getValue()); // 获取值
// editor.setValue("#123") // 设置值
var title = jQuery("#title");
if(title.val() == "") {
alert("请先输入值");
return;
}
var content = editor.getValue();
if(content == "") {
alert("请先输入正文");
return;
}
jQuery.ajax({
url:"/art/update",
type:"post",
data:{
"aid": aid,
"title": title.val(),
"content": content
},
success:function(result) {
if(result.code == 200) {
alert("修改成功");
location.href = "myblog_list.html";
} else {
alert("提交失败,请稍后再试");
}
}
});
}
Mybatis SQL 语句
用户表操作
<?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="add">
insert into userinfo(username, password) values(#{username}, #{password})
</insert>
<select id="login" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username = #{username} and password = #{password}
</select>
<select id="getMyinfo" resultType="com.example.demo.model.UserInfo">
select u.*, count(a.id) as articlecount from userinfo u left join articleinfo a
on u.id = a.uid where u.id = #{id} group by u.id
</select>
<select id="getUserInfoByUsername" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username = #{username}
</select>
<select id="getinfobyid" resultType="com.example.demo.model.UserInfo">
select u.*, count(a.id) as articlecount from userinfo u left join articleinfo a
on u.id = a.uid where u.id = #{id} group by u.id
</select>
<update id="updatePhoto">
update userinfo set photo = #{url} where id = #{id};
</update>
<select id="getUrlbyid" parameterType="java.lang.Integer" resultType="java.lang.String">
select photo from userinfo where id = #{id};
</select>
</mapper>
文章表操作
<?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="getMylist" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo where uid = #{id}
</select>
<delete id="myDel">
delete from articleinfo where id = #{id}
</delete>
<select id="getArticleDetail" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo where id = #{id}
</select>
<insert id="mySub">
insert into articleinfo(title, content, uid) values(#{title}, #{content}, #{uid})
</insert>
<update id="myUpdate">
update articleinfo set title = #{title}, content = #{content} where id = #{aid} and uid = #{uid}
</update>
<select id="getList" resultType="com.example.demo.model.ArticleInfo">
select * from articleinfo limit #{pageSize} offset #{offset}
</select>
<select id="getTotalPages" resultType="java.lang.Integer">
select count(*) from articleinfo
</select>
</mapper>