【Spring练习项目】博客系统

1.项目展示

项目已经发布到云服务器上,想要使用的小伙伴可以点击下面这个链接:

博客项目

由于目前没有实现注册功能,所以这里直接提供一个账号,用以登录:

账号:lisi
密码:123456

2.项目结构设计

后端框架:SpringBoot
数据库:mybatis
前后端交互:ajax

3.项目功能设计

主要功能如下图所示:

在这里插入图片描述

一共四个页面:
在这里插入图片描述
登录页面:

在这里插入图片描述

博客列表页面:

在这里插入图片描述
博客详情页:
在这里插入图片描述

写博客页面:

在这里插入图片描述

4 数据库准备

4.1 建表

一共两张表:

  • user(用户表)

  • blog(博客表)

在这里插入图片描述

创建数据库:

create database if not exists `java_blog_spring` charset utf8mb4;

创建user表:

drop table if exists `java_blog_spring`.`user`;
CREATE TABLE `java_blog_spring`.`user` (
 `id` INT NOT NULL AUTO_INCREMENT,
 `user_name` VARCHAR(128) NOT NULL,
 `password` VARCHAR(128) NOT NULL,
 `github_url` VARCHAR(128) NULL,
 `delete_flag` TINYINT(4) NULL DEFAULT 0,
 `create_time` TIMESTAMP NULL DEFAULT current_timestamp(),
 PRIMARY KEY (`id`),
 UNIQUE INDEX `user_name_UNIQUE` (`user_name` ASC))
ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '⽤户表';

创建blog表:

drop table if exists `java_blog_spring`.`blog`;
CREATE TABLE `java_blog_spring`.`blog` (
 `id` INT NOT NULL AUTO_INCREMENT,
 `title` VARCHAR(200) NULL,
 `content` TEXT NULL,
 `user_id` INT(11) NULL,
 `delete_flag` TINYINT(4) NULL DEFAULT 0,
 `create_time` TIMESTAMP NULL DEFAULT current_timestamp(),
 PRIMARY KEY (`id`))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';

增加一些测试数据:

insert into `java_blog_spring`.`user` (`user_name`, `password`,`github_url
`)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java4
5");
insert into `java_blog_spring`.`user` (`user_name`, `password`,`github_url
`)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");
insert into `java_blog_spring`.`blog` (`title`,`content`,`user_id`) values
("第⼀篇博客","111我是博客正⽂我是博客正⽂我是博客正⽂",1);
insert into `java_blog_spring`.`blog` (`title`,`content`,`user_id`) values
("第⼆篇博客","222我是博客正⽂我是博客正⽂我是博客正⽂",2);

4.2 DB相关数据

DB相关查询:

  1. 获取所有博客列表
  2. 根据博客Id获取博客详情
  3. 插⼊博客
  4. 更新博客
  5. 根据id查询user信息
  6. 根据name查询user信息

User类:

@Data
public class User {
    private Integer id;
    private String userName;
    private String password;
    private String githubUrl;
    private Byte deleteFlag;
    private Date createTime;
}

Blog类:

@Data
public class Blog {
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private Integer deleteFlag;
    private Date createTime;
    //是否为登录用户,1表示为登录用户
    private Integer loginUser;

    public String getCreateTime() {
        //对时间进行格式化
        return DateUtils.formatDate(createTime);
    }
}

UserMapper类:

@Mapper
public interface UserMapper {
    @Select("select id,user_name,password,github_url,delete_flag,create_time from user where delete_flag=0 and id=#{id}")
    User selectById(Integer id);

    @Select("select id,user_name,password,github_url,delete_flag,create_time from user where delete_flag=0 and user_name=#{name}")
    User selectByName(String name);
}

BlogMapper类:

@Mapper
public interface BlogMapper {

    @Select("select * from blog where delete_flag=0")
    List<Blog> selectAllBlog();

    @Select("select * from blog where delete_flag=0 and id=#{blogId}")
    Blog selectBlogById(Integer blogId);

    Integer updateBlog(Blog blog);

    @Insert("insert into blog(title,content,user_id) values (#{title},#{content},#{userId})")
    Integer insertBlog(Blog blog);
}

BlogMapper.xml:

<mapper namespace="com.example.springblog.mapper.BlogMapper">
    <update id="updateBlog">
        update blog
        <set>
            <if test="title!=null">
                title=#{title},
            </if>
            <if test="content!=null">
                content=#{content},
            </if>
            <if test="userId!=null">
                user_id=#{userId},
            </if>
            <if test="deleteFlag!=null">
                delete_flag=#{deleteFlag},
            </if>
        </set>
        where id=#{id}
    </update>
</mapper>

5.项目模块

在这里插入图片描述

6.添加项目公共模块

6.1 common

统一异常抽取为一个类:

@Data
public class Result {
    //业务处理状态码  200成功  <=0失败
    private Integer code;
    //业务返回提示信息
    private String msg;
    //业务返回数据
    private Object data;

    /**
     * 失败时处理内容
     * @return
     */
    public static  Result fail(Integer code,String msg) {
        Result result=new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData("");
        return result;
    }

    public static  Result fail(Integer code,String msg,Object data) {
        Result result=new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    /**
     * 业务处理成功
     * @param data
     * @return
     */
    public static  Result success(Object data) {
        Result result=new Result();
        result.setCode(200);
        result.setMsg("");
        result.setData(data);
        return result;
    }

    public static  Result success(String msg,Object data) {
        Result result=new Result();
        result.setCode(200);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
}

出错时统一异常处理:

@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler
    public Result error(Exception e){
        return Result.fail(-1,e.getMessage());
    }
}

数据统一返回格式:

@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 Result){
            return body;
        }
        if(body instanceof String){
            ObjectMapper objectMapper=new ObjectMapper();
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

6.2 实现前端界面

把之前写好的博客系统静态⻚⾯拷⻉到static⽬录下:

在这里插入图片描述

7.功能实现

7.1实现博客列表

约定前后端交互接口

[请求]
/blog/getlist
[响应]
[
 {
 blogId: 1,
 title: "第⼀篇博客",
 content: "博客正⽂",
 userId: 1,
 postTime: "2021-07-07 12:00:00"
 },
 {
 blogId: 2,
 title: "第⼆篇博客",
 content: "博客正⽂",
 userId: 1,
 postTime: "2021-07-07 12:10:00"
 },
 ...
]

我们约定, 浏览器给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给浏览器返回了⼀个 JSON 格式的数据.

实现服务器代码

在 BlogController 中添加⽅法:

@Slf4j
@RequestMapping("/blog")
@RestController
public class BlogController {

    @Autowired
    private BlogService blogService;

    @RequestMapping("/getlist")
    public List<Blog> getBlogList(){
        return blogService.selectAllBlog();
    }
}

在BlogService 中添加⽅法:

public class BlogService {

    @Autowired
    private BlogMapper blogMapper;

    public List<Blog> selectAllBlog(){
        return blogMapper.selectAllBlog();
    }
}

部署程序, 验证服务器是否能正确返回数据 (使⽤ URL http://127.0.0.1:8080/blog/getlist 即可)

实现客户端代码

修改 blog_list.html, 删除之前写死的博客内容, 并新增js 代码处理 ajax 请求.

    <script src="./js/jquery.min.js"></script>
    <script src="./js/common.js"></script>
    <script>
        $.ajax({
            type:"get",
            url:"/blog/getlist",
            success:function(result){
                if(result.code==200 && result.data!=null && result.data.length>0){
                    var blogs=result.data;
                    var finalHtml="";
                    for(var blog of blogs){
                        finalHtml += '<div class="blog">';
                        finalHtml += '<div class="title">'+blog.title+'</div>'
                        finalHtml += '<div class="date">'+blog.createTime+'</div>'
                        finalHtml += '<div class="desc">'+blog.content+'</div>'
                        finalHtml += '<a class="detail" href="blog_detail.html?blogId='+blog.id+'">查看全文&gt;&gt;</a>'
                        finalHtml += '</div>'
                    }
                    $(".right").html(finalHtml);
                }
            },
            error:function(error){
                console.log(error);
                if(error!=null && error.status==401){
                    //用户未登录
                    location.assign("blog_login.html");
                }
            }
        });
        var url="/user/getUserInfo";
        getUserInfo(url);
    </script>

7.2实现博客详情

⽬前点击博客列表⻚的 “查看全⽂” , 能进⼊博客详情⻚, 但是这个博客详情⻚是写死的内容. 我们期望能够根据当前的 博客 id 从服务器动态获取博客内容.

约定前后端交互接口

/blog/getBlogDetail?blogId=1
[响应]
{
 blogId: 1,
 title: "第⼀篇博客",
 content: "博客正⽂",
 userId: 1,
 postTime: "2021-07-07 12:00:00"
}

实现服务器代码

在 BlogController 中添加getBlogDeatail ⽅法:

   /**
     * 获取博客详情
     * @param blogId
     * @return
     */
    @RequestMapping("/getBlogDetail")
    public Result getBlogDetail(Integer blogId,HttpSession session){
        log.info("blogId:"+blogId);
        if(blogId == null){
            return Result.fail(-1,"非法博客id");
        }
        Blog blog=blogService.selectBlogById(blogId);
        //获取登录用户信息
        User loginUser=(User) session.getAttribute(Constants.USER_INFO_SESSION);
        //判断登录用户和博客作者是否是同一个人
        if(loginUser!=null && loginUser.getId()== blog.getUserId()){
            blog.setLoginUser(1);
        }
        return Result.success(blog);
    }

在BlogService 中添加getBlogDeatil⽅法:

    public Blog selectBlogById(Integer blogId){
        return blogMapper.selectBlogById(blogId);

    }

实现客户端代码

    <script>
        $.ajax({
            type:"get",
            url:"/blog/getBlogDetail"+location.search,
            success:function(result){
                if(result.code==200 && result.data!=null){
                    var blog=result.data;
                    $(".title").text(blog.title);
                    $(".date").text(blog.createTime);
                    editormd.markdownToHTML("content", {
                        markdown: blog.content ,
                    });
                    //$(".detail").text(blog.content);
                    if(blog.loginUser==1){
                        var html="";
                        html+= '<button onclick="window.location.href='blog_update.html?blogId='+blog.id+''">编辑</button>';
                        html+='<button onclick="deleteBlog()">删除</button>';
                        $(".operating").html(html);
                    }
                }
            },
            error:function(error){
                consolo.log(error);
                if(error!=null && error.status==401){
                    //用户未登录
                    location.assign(blog_login.html);
                }
            }
        });
        var url= "/user/getAuthorInfo" + location.search;
        getUserInfo(url);

common.js代码:

function getUserInfo(url){
    $.ajax({
        type:"get",
        url:url,
        success:function(result){
            if(result!=null && result.code==200 && result.data!=null){
                var user=result.data;
                $(".left .card h3").text(user.userName);
                $(".left .card a").attr("href",user.githubUrl);
            }
        }
    });
}

7.3实现登录

  • 登陆⻚⾯提供⼀个 form 表单, 通过 form 的⽅式把⽤户名密码提交给服务器.

  • 服务器端验证⽤户名密码是否正确. 如果密码正确,

  • 则在服务器端创建 Session , 并把 sessionId 通过 Cookie 返回给浏览器

约定前后端交互接口

[请求]
/user/login
username=test&password=123
[响应]
200 登录成功
<0 登录失败

实现服务器代码

在 UserController 中添加⽅法:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/login")
    public Result login(HttpServletRequest request,String username, String password){
        //参数校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){
            return Result.fail(-1,"用户名密码不能为空");
        }
        //验证密码
        User user=userService.selectByName(username);
        if(user==null || !SecurityUtils.decrypt(password,user.getPassword())){
            return Result.fail(-2,"用户名密码错误");
        }
        //设置session
        HttpSession session= request.getSession(true);
        session.setAttribute(Constants.USER_INFO_SESSION,user);
        return Result.success("登录成功");
    }
}

在UserService 中添加⽅法:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private BlogMapper blogMapper;

    public User selectByName(String name){
        return userMapper.selectByName(name);
    }
}

实现客户端代码

    <script src="./js/jquery.min.js"></script>
    <script>
        function login(){
            $.ajax({
                type:"post",
                url:"/user/login",
                data:{
                    username:$("#userName").val(),
                    password:$("#password").val()
                },
                success:function(result){
                    if(result.code==200){
                        location.href="blog_list.html";
                        return;
                    }else if(result.code<0 && result.msg!=''){
                        alert(result.msg);
                        return;
                    }
                },
                error:{

                }
            });
        }
    </script>

7.4实现强制要求登录

当⽤户访问 博客列表⻚ 和 博客详情⻚ 时, 如果⽤户当前尚未登陆, 就⾃动跳转到登陆⻚⾯.

添加拦截器

登录拦截器:

@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(Constants.USER_INFO_SESSION)!=null){
            //用户已经登录了
            return true; //不拦截
        }
        response.setStatus(401);
        return false;
    }
}

使用拦截器:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    private final List<String> excludePaths = Arrays.asList(
            "/**/*.html",
            "/blog-editormd/**",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/user/login"
    );
    @Autowired
    private LoginInterceptor loginInterceptor;
    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")  //拦截所有路径
                .excludePathPatterns(excludePaths);  //不拦截excludePaths包括的类型文件
    }
}

实现客户端代码

1.修改 blog_datail.html

  • 访问⻚⾯时, 添加失败处理代码
  • 使⽤ location.assign 进⾏⻚⾯跳转.
			error:function(error){
                console.log(error);
                if(error!=null && error.status==401){
                    //用户未登录
                    location.assign("blog_login.html");
                }
            }

2.修改 blog_list.html

  • 访问⻚⾯时, 添加失败处理代码
  • 使⽤ location.assign 进⾏⻚⾯跳转.
            error:function(error){
                consolo.log(error);
                if(error!=null && error.status==401){
                    //用户未登录
                    location.assign(blog_login.html);
                }
            }

7.5实现显示用户信息

  • 如果当前⻚⾯是博客列表⻚, 则显示当前登陆⽤户的信息.
  • 如果当前⻚⾯是博客详情⻚, 则显示该博客的作者⽤户信息.

约定前后端交互接口

在博客列表⻚, 获取当前登陆的⽤户的⽤户信息.

[请求]
/user/getUserInfo
[响应]
{
userId: 1,
username: test
 ...
}

在博客详情⻚, 获取当前⽂章作者的⽤户信息

[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
userId: 1,
username: test
}

实现服务器代码

在 UserController 中添加⽅法:

    /**
     * 获取登录用户信息
     * @return
     */
    @RequestMapping("/getUserInfo")
    public Result getUserInfo(HttpSession session){
        if(session==null || session.getAttribute(Constants.USER_INFO_SESSION)==null){
            return Result.fail(-1,"用户未登录");
        }
        User user=(User)session.getAttribute(Constants.USER_INFO_SESSION);
        return Result.success(user);
    }

    /**
     * 获取博客作者信息
     * @return
     */
    @RequestMapping("/getAuthorInfo")
    public Result getAuthorInfo(Integer blogId){
        if(blogId==null || blogId<=0){
            return Result.fail(-1,"博客不存在~");
        }
        User user=userService.selectAuthorByBlogId(blogId);
        return Result.success(user);
    }

在UserService 中添加⽅法:

    public User selectAuthorByBlogId(Integer blogId){
        User user=null;
        Blog blog=blogMapper.selectBlogById(blogId);
        if(blog!=null && blog.getUserId()>0){
            user=userMapper.selectById(blog.getUserId());
        }
        if(user!=null){
            user.setPassword("");
        }
        return user;
    }

7.6实现用户退出

约定前后端交互接口

[请求]
/user/logout
[响应]
true

实现服务器代码

在 UserController 中添加⽅法:

    /**
     * 注销
     * @return
     */
    @RequestMapping("/logout")
    public Result logout(HttpSession session){
        session.removeAttribute(Constants.USER_INFO_SESSION);
        return Result.success(true);
    }

实现客户端代码

客户端代码, 注销改为⼀个a标签, href 设置为logout, 点击的时候就会发送GET/logout请求

<a class="nav-span" href="#" onclick="logout()">注销</a>

在common.js中添加logout⽅法:

function logout(){
    $.ajax({
        type:"get",
        url:"/user/logout",
        success:function(result){
            if(result!=null && result.data==true){
                location.href="blog_login.html";
            }
        }
    });
}

7.7实现发布博客

约定前后端交互接口

[请求]
/blog/add
title=标题&content=正⽂...
[响应]
true 成功
false 失败

实现服务器代码

在 BlogController 中添加⽅法:

    /**
     * 发布博客
     * @return
     */
    @RequestMapping("/add")
    public Result addBlog(String title, String content,HttpSession session){
        if(!StringUtils.hasLength(title) || !StringUtils.hasLength(content)){
            return Result.fail(-1,"标题或内容不能为空");
        }
        User user= (User) session.getAttribute(Constants.USER_INFO_SESSION);
        if(user==null || user.getId()<=0){
            return Result.fail(-1,"用户不存在");
        }
        try{
            Blog blog=new Blog();
            blog.setTitle(title);
            blog.setContent(content);
            blog.setUserId(user.getId());
            blogService.insertBlog(blog);
        }catch (Exception e){
            return Result.fail(-1,"博客发布失败~");
        }
        return Result.success(true);
    }

在BlogService 中添加⽅法:

 public Integer insertBlog(Blog blog){
        return blogMapper.insertBlog(blog);
    }

实现客户端代码

给提交按钮添加click事件 <input type=“button” value="发布⽂章"id=“submit” οnclick=“submit()”>

        $("#submit").click(function(){
            $.ajax({
                type:"post",
                url:"/blog/add",
                data:{
                    title:$("#title").val(),
                    content:$("#content").val()
                },
                success:function(result){
                    if(result!=null && result.code==200 && result.data==true){
                        location.href="blog_list.html";
                    }else{
                        alert(result.msg);
                    }
                },
                error:function(error){
                    if(error!=null && error.status==401){
                        alert("请先登录!!!");
                    }
                }
            });
        });

7.8实现删除/编辑博客

进⼊⽤户详情⻚时, 如果当前登陆⽤户正是⽂章作者, 则在导航栏中显示 “删除” 按钮, ⽤户点击时则删除该⽂章.

需要实现两件事:

  • 判定当前博客详情⻚中是否要显示 删除 按钮
  • 实现删除逻辑.

约定前后端交互接口

编辑博客

[请求]
/blog?BlogId=1
[响应]
{
 blogId: 1,
 title: "第⼀篇博客",
 content: "博客正⽂",
 userId: 1,
 postTime: "2021-07-07 12:00:00",
 loginUser: 1
}

删除博客

[请求]
GET /blog/delete?blogId=1
[响应]
true 删除成功

实现服务器代码

在 BlogController 中添加⽅法:

    /**
     * 更新博客
     * @param blog
     * @return
     */
    @RequestMapping("/updateBlog")
    public Result updateBlog(Blog blog){
        if(!StringUtils.hasLength(blog.getTitle()) || !StringUtils.hasLength(blog.getContent()) || blog.getId()==null){
            return Result.fail(-1,"标题或内容不合法");
        }
        blogService.updateBlog(blog);
        return Result.success(true);
    }

    /**
     * 删除博客
     * @return
     */
    @RequestMapping("/deleteBlog")
    public Result deleteBlog(Integer blogId){
        if(blogId==null){
            return Result.fail(-1,"博客不存在~");
        }
        Blog blog=new Blog();
        blog.setId(blogId);
        blog.setDeleteFlag(1);
        blogService.updateBlog(blog);
        return Result.success(true);
    }
}

在BlogService 中添加⽅法:

    public Integer updateBlog(Blog blog){
        return blogMapper.updateBlog(blog);
    }

实现客户端代码

删除博客:

        function deleteBlog(){
            $.ajax({
                type:"post",
                url:"/blog/deleteBlog" + location.search,
                success:function(result){
                    if(result!=null && result.code==200 && result.data==true){
                        location.href="blog_list.html";
                    }else{
                        alert(result.msg);
                    }
                },
                eeror:function(error){
                    if(error!=null && error.status==401){
                    //用户未登录
                    location.assign("blog_login.html");
                }
                }
            });
        }

编辑博客:

        //获取博客的详细内容,并且反应到页面上
        $.ajax({
            type:"get",
            url:"/blog/getBlogDetail" + location.search,
            success:function(result){
                if(result!=null && result.code==200 && result.data!=null){
                    var blog=result.data;
                    $("#blogId").val(blog.id);
                    $("#title").val(blog.title);
                    $("#content").val(blog.content);
                }else if(result!=null){
                    alert(result.msg);
                }
            },
            error:function(error){
                if(error!=null && error.status==401){
                    //用户未登录
                    location.assign(blog_login.html);
                }
            }
        });

        $("#submit").click(function(){
            $.ajax({
                type:"post",
                url:"/blog/updateBlog",
                data:{
                    id:$("#blogId").val(),
                    title:$("#title").val(),
                    content:$("#content").val()
                },
                success:function(result){
                    if(result!=null && result.code==200 && result.data==true){
                        location.href="blog_list.html";
                    }else if(result!=null){
                        alert(result.msg);
                    }
                }
            });
        });
        
    </script>

7.9实现加密加盐

加密工具类

使用md5进行密码加密:

public class SecurityUtils {
    /**
     * 加密
     * 根据明文,返回密文(salt+加密后的密文)
     * @return
     */
    public static String encry(String inputPassword){
        //生成盐值
        String salt= UUID.randomUUID().toString().replace("-","");
        //md5加密(明文+盐值)
        String password= DigestUtils.md5DigestAsHex((inputPassword+salt).getBytes());
        return salt+password;
    }

    /**
     * 验证密码是否正确
     * @return
     */
    public static boolean decrypt(String inputPassword,String finalPassword){
        //判空
        if(!StringUtils.hasLength(inputPassword) || !StringUtils.hasLength(finalPassword)){
            return false;
        }
        //验证长度
        if(finalPassword.length()!=64){
            return false;
        }
        //验证密码
        String salt=finalPassword.substring(0,32);
        String password= DigestUtils.md5DigestAsHex((inputPassword+salt).getBytes());
        return (salt+password).equals(finalPassword);
    }
}

使用

        //验证密码
        User user=userService.selectByName(username);
        if(user==null || !SecurityUtils.decrypt(password,user.getPassword())){
            return Result.fail(-2,"用户名密码错误");
        }

修改数据库密码

使⽤测试类给密码123456⽣成密⽂:
e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2d7f8971216e39b7

执行SQL:

update user set password='e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2
d7f8971216e39b7' where id=1;