Java博客系统搭建(Spring + SpringBoot + MyBatis)

项目概述

项目名称:知识共享小屋
项目描述:该博客系统使用Spring Boot框架开发,旨在实现用户注册、登录、查看、修改、发布、删除文章、强制登录等功能。
开发环境:MacOS15、IDEA专业版、JDK17、Navicat、Termius
应用技术:Spring 、SpringBoot、MyBatis、MySQL、JWT、Ajax

在这里插入图片描述

一、项目架构(框架、配置文件)

项目框架

工欲善其事,必先利其器
因此先将项目架构搭建好,主要分为controller、mapper、model、config、util、、constants、interceptor几个包,并没有太过于复杂的逻辑便省略service。
在这里插入图片描述

YML配置

相对于properties,yml采用缩进来表示结构层次,使其更具有可读性

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

# mybatis配置
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl         # mybatis打印执行SQL语句配置
    map-underscore-to-camel-case: true                            #自动驼峰转换 eg: user_name ->  userName

# 日志配置
logging:
  file:                                             # 储存打印日志
    name: logs/blog.log

二、数据库设计 (结构、SQL、实体)

结构设计

在这里插入图片描述

SQL语句

创建数据库代码放在resource文件夹下,方便后期在云服务器上创建数据库,将SQL语句放在Navicat中执行。

-- 创建数据库
CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4;

-- 使用数据库
USE blog;

-- 用户表
DROP TABLE IF EXISTS user;
CREATE TABLE user (
                      `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
                      `user_name` VARCHAR(128) NOT NULL,
                      `password` VARCHAR(128) NOT NULL,
                      `github` VARCHAR(128) NULL,
                      `delete_flag` TINYINT(4) NULL DEFAULT 0,  -- 采用物理删除方式,默认0显示,1不显示
                      `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
                      `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  -- 更改后时间
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '用户表';

-- 博客表
DROP TABLE IF EXISTS article;
CREATE TABLE article (
                         `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
                         `title` VARCHAR(255) NOT NULL,
                         `content` TEXT NOT NULL,  -- 使用TEXT类型存储文章内容
                         `user_id` INT NOT NULL,   -- 文章作者id
                         `delete_flag` TINYINT(4) NULL DEFAULT 0,  -- 采用物理删除方式,默认0显示,1不显示
                         `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
                         `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  -- 更改后时间
                         FOREIGN KEY (user_id) REFERENCES user(id)  -- 通过user_id作为外键和user表中id关联,从而查看作者信息
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '文章表';

-- 新增用户信息
INSERT INTO user (user_name, password, github) VALUES ("zhangsan", "123456", "https://gitee.com/1");
INSERT INTO user (user_name, password, github) VALUES ("lisi", "123456", "https://gitee.com/2");

-- 新增博客文章
INSERT INTO article (title, content, user_id) VALUES ("第一篇博客", "111我是博客正文我是博客正文我是博客正文", 1);
INSERT INTO article (title, content, user_id) VALUES ("第二篇博客", "222我是博客正文我是博客正文我是博客正文", 2);

实体类

在model文件下创建user、article实体类,进行数据封装便于对数据库数据进行管理和操作,按照数据表进行变量创建。
在这里插入图片描述

@Data
public class UserInfo {
    private Integer id;
    private String userName;
    private String password;
    private String github;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}
@Data
public class BlogInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
    private boolean isAuthor;

    public String getCreateTime() {             //重写get方法,对默认时间进行格式化
        return DateUtils.format(createTime);
    }
}

注:在BlogInfo表中调用DataUtils中format方法,对createTime的get方法进行重写,确保以正确的时间格式显示。

封装数据库操作

先将大体需要调用数据库哪些操作进行编写,方便后续调用,同样分为user和blog操作进行划分,在mapper下创建UserMapper、BlogMapper。
UserMapper:

@Mapper
public interface UserMapper {

    //注册用户插入数据库user表中
    @Insert("insert into user(user_name, password) values (#{userName}, #{password})")
    Integer insertUser(String userName, String password);

    //通过id查询用户信息
    @Select("select * from user where delete_flag = 0 and id = #{id}")
    UserInfo selectById(Integer id);

    //通过用户名查询用户信息,验证用户密码是否正确
    @Select("select * from user where delete_flag = 0 and user_name = #{userName}")
    UserInfo selectByUserName(String userName);

}

BlogMapper:

@Mapper
public interface BlogMapper {
    //查询所有博客信息
    @Select("select * from article where delete_flag = 0")
    List<BlogInfo> selectAllBlog();

    //通过博客id查询博客信息
    @Select("select * from article where delete_flag = 0 and id = #{blogId}")
    BlogInfo selectByBlogId(Integer blogId);

    //将默认delete_flag的0,修改成1进行物理删除
    @Update("update article set delete_flag = 1 where id = #{blogId}")
    boolean delete(Integer blogId);

    //将博客信息插入到blog表中
    @Insert("insert into article(title, content, user_id) values (#{title}, #{content}, #{userId})")
    Integer insertBlog(BlogInfo blogInfo);

    //更新博客信息需要判断当前参数是否为空,通过mybatis的注解方式来实现此方法过于繁琐,不利于查看,通常使用xml的方式进行编写
    //在此只有一个数据更改较为繁琐,便不使用xml方式
    @Update({
            "<script>",
            "UPDATE article",
            "<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}",
            "</script>"
    })
    Integer updateBlog(BlogInfo blogInfo);

}

三、Utils(Date、Security、JWT)

DateUtil

数据库中current_timestamp默认时间格式为 2024-07-15T14:20:00Z格式,因此要将其转换为 YY-MM-DD HH-mm-ss格式,通过java.text下的SimpleDateFormat方法来进行格式转换。

public class DateUtils {
    // 定义时间默认格式
    private static final String DEFAULT_DATE_TIME = "yyyy-MM-dd HH:mm:ss";

    //根据默认格式返回时间
    public static String format(Date date) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DEFAULT_DATE_TIME);
        return simpleDateFormat.format(date);
    }
}

SecurityUtils

用户隐私信息在数据库中以明文的方式进行存储,数据库信息泄漏隐私显而易见,通过md5的加密方式来对数据进行存储。此项目中对用户注册密码进行加密,用户登录进行校验。
在这里插入图片描述

public class SecurityUtils {
    /**
     * 将注册密码和通过UUID随机生成的盐值(salt)进行mad加密,返回salt目的方便后续解密操作
     * @param password
     * @return salt + md5(salt + password)
     */
    public static String encrypt(String password) {
        String salt = UUID.randomUUID().toString().replace("-", "");
        String result = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        return salt + result;
    }

    /**
     * 加密方法由salt + md5(salt + password)组成六十四位字符进行存储,因此前32位则是salt,
     * salt为固定不变的,在将salt和新输入的password进行md5加密,
     * 最后将salt和加密后的密码拼接在一起与数据库中存储的加密密码进行比比对,相同则密码正确。
     * @param password
     * @param sqlPassword
     * @return boolean
     */
    public static boolean verify(String password, String sqlPassword) {
        if(!StringUtils.hasLength(password)) {
            return false;
        }
        if(sqlPassword == null || sqlPassword.length() != 64) {
            return false;
        }

        String salt = sqlPassword.substring(0, 32);
        String result = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        return (salt + result).equals(sqlPassword);
    }
}

JWTUtils

JWT主要应用于信息交换、身份校验,适用于分布式系统中跨域身份验证问题,主要由头部(Header)、载荷(Payload)、签名(Singnature)三部分组成。
在这里插入图片描述
在pom.xml文件中导入JWT依赖,创建JWTUtils对生成、解析功能进行封装

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.3</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.3</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.3</version>
            <scope>runtime</scope>
        </dependency>
public class JWTUtils {
    // 定义JWT过期时间(单位为ms)此处设为一小时
    private static final long JWT_EXPIRATION = 60 * 60 * 1000;

    // 设置签名密钥,通过Keys.secretKeyFor(Keys.HmacShaKeySize.of(256))方法来随机生成
    // 可以通过随机生成密钥进一步提高安全性
    private static final String KEY = "r2VCxiEsp+Z8W/UZHHJdHoK9VcDbp7KQZNt2+zfQw0Q=";

    //通过对header和payload进行加密,生成签名
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(KEY));

    /**
     *通过jsonwebtoken中的方法,设置声明、过期时间、签名生成token
     * @param claims
     * @return token
     */
    public static String genJWT(Map<String, Object> claims) {
        String token = Jwts.builder().setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
                .signWith(SECRET_KEY)
                .compact();
        return token;
    }

    /**
     * 设置解析签名,构造解析器,解析token中的声明部分
     * @param token
     * @return 声明(body)
     */
    public static Claims parserJWT(String token) {
        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
        Claims body = jwtParserBuilder.build().parseClaimsJws(token).getBody();
        return body;
    }

    /**
     * 调用parseJWT方法付对token进行解析,后去声明中的id,方便后续通过token查看用户id
     * @param token
     * @return 用户ID(userID)
     */
    public static Integer getIdByToken(String token) {
        Claims claims = parserJWT(token);
        if(claims != null) {
            Integer userId = (Integer) claims.get(Constants.TOKEN_ID);
            if(userId > 0){
                return userId;
            }
        }
        return null;
    }
}

四、统一结果、异常返回

对响应结果和异常进行统一规划,确保了所有接口返回的结果处理的异常都是一致的,提高了代码的可读性,简化了客户端的处理逻辑。

统一结果实体类

在这里插入图片描述

@Data
public class Result<T> {        //通过泛型来处理,避免了类型转换的的问题
    private int code;
    private String message;
    private T data;

    /**
     * 业务逻辑执行成功
     * @param data
     * @return
     */
    public static <T> Result <T> success(T data) {
        Result result = new Result();
        result.setCode(200);
        result.setMessage("");
        result.setData(data);
        return result;
    }

    /**
     * 业务逻辑处理失败,两种处理方法判断是否需要数据进行处理
     * @param data、message
     * @param message
     * @return
     */

    public static <T> Result<T> fail(String message) {
        Result result = new Result();
        result.setCode(500);
        result.setMessage(message);
        return result;
    }

    public static <T> Result<T> fail(T data, String message) {
        Result result = new Result();
        result.setCode(500);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}


统一结果封装
@ControllerAdvice           //应用于所有响应和异常的spring注解
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;      //Jackson库中的类,用于处理JSON序列化和反序列化

    //判断响应是否处理,true or false;
    @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) {           //响应类型为Result类型,不做处理直接返回
            return body;
        }
        if (body instanceof String) {           //响应结果为String,利用ObjectMapper将其转换成JSON格式
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);            //响应结果为其他,转换为Result格式
    }
}

二者主要的区别在于,前者规定了统一返回的格式,后者则规定响应由什么格式返回。

统一异常处理
@Slf4j
@ControllerAdvice
public class ExceptionHandle {
    // 捕获程序中未处理的异常,通过统一格式进行返回,不会暴露更多信息
    @ExceptionHandler
    public Object ExceptionHandle(Exception e) {
        log.error("异常路径" + e.getMessage());
        return Result.fail("内部错误,请联系管理员");
    }
}

五、注册、登录、注销登录

在这里插入图片描述

注册

客户端访问blog_register.html页面,输入注册注册信息通过Ajax发送http请求,将参数传递到服务器,服务器经过md5加密后通过mybatis于数据库进行交互。
客户端代码:

<script>
        function register() {
            $.ajax({
                url: "user/register",
                type: "post",
                data: {
                    userName : $("#username").val(),
                    password : $("#password").val()
                },
                success : function (result) {       //服务器正确响应,通过响应数据进行页面跳转
                    if (result.code == 200 && result.data == "success") {
                        alert("注册成功!");
                        location.replace("/blog_login.html");
                    } else {
                        alert(result.errMessage);
                    }
                },
                error : function (err) {
                    if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                        location.replace("/blog_login.html");
                    }
                }
            })
        };
    </script>
登录

登录逻辑同理于注册页面,不同之处在于服务器接受参数后查询用户密码,进行解密验证密码是否正确,正确则跳转到博客详情页面。
客户端代码:

<script>
        function login() {
            $.ajax({
                url : "user/login",
                type : "get",
                data: {
                    userName : $("#username").val(),
                    password : $("#password").val()
                },
                success : function (result) {       //服务器正确响应,通过响应数据进行页面跳转
                    if (result.code == 200 && result.data != "") {    //成功登录后,data中存储的为token信息,只需判断是否为空即可
                        localStorage.setItem("user_token", result.data);        //成功登录后,将服务器设置的token存储到客户端中,并在common.js创建公用函数,将token放在请求头中
                        location.replace("/blog_list.html");
                    } else {
                        alert(result.errMessage);
                        location.replace("bolg_login.html");
                    }
                },
            })
        }
        function register() {
            location.replace("/blog_register.html");
        }
    </script>

Token存储
创建common.js用于存放共用函数,创建一个全局函数,通过此函数将token放在请求头中,确保每次发送ajax请求时都会携带token,方便服务器进行验证。

$(document).ajaxSend(function (e, xhr, opt){
    var item = localStorage.getItem("user_token");      //从本地存储中获取名为"user_token"的元素
    xhr.setRequestHeader("user_token",item);                    //将该元素携带的token放到请求头中,方便服务器进行验证
})

登录、注册服务器代码:
接受客户端传递的参数,通过SecurityUtils工具进行加密解密,成功登录后将用户信息存储通过JWTUtils存储到token中,跳转到博客列表页面。

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping("/register")
    public Result register(String userName, String password) {
        //判断用户是否正确输入注册信息
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            return Result.fail("用户名或密码为空,请重新输入!");
        }
        //信息正确通过调用userMapper实现数据存储
        try{
            //通过md5对用户密码进行加密,在存储到数据库中
            String encryptPassword = SecurityUtils.encrypt(password);
            Integer result = userMapper.insertUser(userName, encryptPassword);
            return Result.success("success");
        } catch (NullPointerException e) {
            //可能插入失败返回用户为空,手动处理处理异常,不通过统一异常进行返回,手动返回出错信息
            log.error("错误信息:", e);
            return Result.fail("用户名已存在,请重新输入");
        }
    }

    @GetMapping("/login")
    public Result login(String userName, String password) {
        //判断输入格式是否正确
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            return Result.fail("用户名或密码为空,请重新输入!");
        }
        try {
            //通过调用verify验证密码
            UserInfo userInfo = userMapper.selectByUserName(userName);
            boolean result = SecurityUtils.verify(password, userInfo.getPassword());
            if(!result) {
                return Result.fail("密码错误,请重新输入!");
            }
            //登录成功,同JWT将用户信息存储在token中
            Map<String, Object> map = new HashMap<>();
            map.put(Constants.TOKEN_ID, userInfo.getId());
            map.put(Constants.TOKEN_USERNAME, userInfo.getUserName());
            String token = JWTUtils.genJWT(map);
            return Result.success(token);
        }catch (NullPointerException e){
            //登录失败返回用户为空,手动处理处理异常,不通过统一异常进行返回,手动返回出错信息
            log.error("错误信息:", e);
            return Result.fail("用户名不存在,请重新输入!");
        }
    }
}

注销登录

在用户登录后所有页面都显示注销登录按钮,单用户点击此按钮时触发登录事件,删除用户tokon退回到登录页面,此功能为通用功能因此写在common.js文件下。

function logout() {
    localStorage.removeItem("user_token");      //用户退出登录后,移除当前所存在的token
    location.href = "blog_login.html";          //回到登录页面
}

六、文章列表页面

在这里插入图片描述

个人信息

用户登录成功后个人信息存储在token当中,客户端向服务器发起ajax请求,客户端从token获取用户信息进行返回。
在这里插入图片描述
客户端代码:
后续还要对博客作者信息进行更新,因此直接将此内容封装成一个函数到commom.js文件中,通过url作为参数进行传递,在博客列表页面直接调用getInfo(user/getUserInfo)函数进行信息交互。

function getInfo(url) {
    $.ajax({
        url: url,
        type: "get",
        success : function (result) {
            if(result.code == 200 && result.data != "") {           //从服务器获取用户信息,对页面内容进行更改
                $(".container .left .card h3").text(result.data.userName);
                $(".container .left .card a").attr("href", result.data.github);
            } else {
                alert(result.errMessage);
                location.replace("/blog_login.html");
            }
        },
        error : function (err) {       //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
            if(err.status == 401) {
                location.replace("/blog_login.html");
            }
        }
    })
}

服务器代码:
用户信息更新,同样放在UserController接口下,创建getUserInfo接口进行调用。

@GetMapping("/getUserInfo")
    public Result getUserInfo(HttpServletRequest request) {
        try{
            //从request请求头中获取token,从中获取用户ID,通过用户ID获取用户信息进行返回
            String token = request.getHeader("user_token");
            Integer userId = JWTUtils.getIdByToken(token);
            UserInfo userInfo = userMapper.selectById(userId);
            System.out.println(token);
            return Result.success(userInfo);
        }catch (NullPointerException e) {
            log.error("token错误:", e);
            return Result.fail("用户未登录,重新登录!");
        }
    }
博客列表

用户成功登录进入到博客详情页面,通过服务器调用数据库blog表信息,将所有博客信息返回到客户端进行显示。
在这里插入图片描述
客户端代码:
服务器返回博客信息列表,依次通过遍历进行赋值,最后拼接到列表详情中。

 <script>
        getInfo("/user/getUserInfo");               //调用common.js中的getInfo函数,发送ajax请求从服务器获取登录用户信息
        $.ajax({
            url: "blog/getList",
            type: "get",
            success: function (result) {
                if(result.code == 200 && result.data != "") {
                    var blogs = result.data;        //将博客List列表存储到blogs变量当中,简化代码量
                    var finalHtml = '';             //定义变量用于拼接博客信息
                    for(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>';
                    }
                    $(".container .right").html(finalHtml);         //将最后的html页面拼接到博客详情下
                }
            },
            error : function (err) {
                if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                    location.replace("/blog_login.html");
                }
            }
        });
    </script>

服务器代码:
在controller包下创建BlogController类,用于存放关于博客相关操作接口。

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

    @Autowired
    private BlogMapper blogMapper;

    @GetMapping("/getList")
    public Result getList() {
        List<BlogInfo> blogInfoList = blogMapper.selectAllBlog();
        return Result.success(blogInfoList);
    }
}

七、文章详情页面

在博客列表页面点击全文进入博客详情页面,在页面显示博客作者信息、文章详细信息、判断登录用户是否为博客信息作者,是否可以显示删除按钮进行删除操作。
在这里插入图片描述

作者个人信息

与博客列表页面显示登录用户信息大同小异,唯一区别在与客户端和服务器说调用的接口不同。
在这里插入图片描述

客户端代码:
通过location.search传递查询字符串给服务器,服务器接受参数通过博客id进行查询博客信息。

<script>
        //显示博客作者信息
        var userUrl = "/user/getAuthorInfo" + location.search;      //调用location的search方法,获取url中查询字符串部分,?及以后地址
        getInfo(userUrl);           //调用common.js里getInfo方法
    </script>

服务器代码:

@GetMapping("/getAuthorInfo")
    public Result getUserInfo(Integer blogId) {
        try{			//通过博客id查询博客信息,通过博客信息里的用户id查询用户信息
            BlogInfo blogInfo = blogMapper.selectByBlogId(blogId);
            UserInfo userInfo = userMapper.selectById(blogInfo.getUserId());
            return Result.success(userInfo);
        }catch (NullPointerException e) {
            log.error("用户信息异常:", e);
            return Result.fail("获取作者信息失败!");
        }
    }
博客信息、删除权限

显示博客信息同上文显示博客列表,根据服务器返回响应进行赋值,因为要判断是否有删除权限,需要在blogInfo中增加布尔类型isAuthor变量来确定是否有删除权限。
在这里插入图片描述

客户端代码:

 <script>
        getInfo("/user/getAuthorInfo" + location.search);           //调用location的search方法,获取url中查询字符串部分,?及以后地址
        $.ajax({
            url: "blog/getBlogDetail" + location.search,        //获取当前博客id
            type: "get",
            success: function (result){                         //从响应中获取博客信息,依次进行赋值
                if(result.code == "200" && result.data != ""){
                    var blog = result.data;
                    $(".container .title").text(blog.title);
                    $(".container .date").text(blog.createTime);
                    $(".container .detail").text(blog.content)
                }
                if(result.data.author == true){                 //判断用户是否为作者本人,决定删除权限
                    var html = "";
                    html += '<button οnclick="window.location.href=\'blog_update.html?blogId=' + blog.id + '\'">编辑</button>';
                    html += '<button οnclick="deleteBlog(' + blog.id + ')">删除</button>';
                    $(".container .operating").html(html);
                }
            },
            error : function (err) {       //对于异常响应进行判断
                if(err.status == 401) {
                    location.replace("/blog_login.html");
                }
            }
        });

        function deleteBlog() {
            if(confirm("确认删除?")){       //添加提示框,询问用户是否确定删除
                $.ajax({
                    type: "post",
                    url : "/blog/delete" + location.search,
                    success: function (result) {
                        if(result.code == 200 || result.data == "success"){
                            alert("删除成功");
                            location.replace("/blog_list.html");
                        }else {
                            alert(result.errMessage);
                        }
                    },
                    error : function (err) {
                        if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                            location.replace("/blog_login.html");
                        }
                    }
                })
            }
        };
    </script>

服务器代码:

 @GetMapping("/getBlogDetail")
    public Result getBlogDetail(Integer blogId, HttpServletRequest request) {
        //通过博客id查找博客信息,从博客信息中获取用户信息,
        //从token中获取用户信息,判断token中用户信息和博客信息中用户信息是否相等,设置权限
        BlogInfo blogInfo = blogMapper.selectByBlogId(blogId);
        String token = request.getHeader("user_token");
        Integer userId = JWTUtils.getIdByToken(token);
        if(blogInfo.getUserId().equals(userId)){
            blogInfo.setAuthor(true);		//是同一名用户设置为true
            return Result.success(blogInfo);
        } else {
            blogInfo.setAuthor(false);		//不是则为false
            return Result.success("false");
        }
    }

    @PostMapping("/delete")
    public Result delete(Integer blogId) {
        //采用物理删除的方式,默认0为正常显示,更改为1表示删除
        boolean delete = blogMapper.delete(blogId);
        if(delete) {
            return Result.success("success");
        } else {
            return Result.fail("删除失败!");
        }
    }

八、文章更改页面

在这里插入图片描述

发布新博客

用户登录后点击写博客进入blog_edit.html页面,将输入的文章标题、内容通过json的格式传递给服务器进行处理。
在这里插入图片描述

服务器代码:

<script type="text/javascript">

        $(function () {                             //编辑文档官方说明
            var editor = editormd("editor", {
                width: "100%",
                height: "550px",
                path: "blog-editormd/lib/"
            });
        });

        function submit() {
            $.ajax({
                type: "post",
                url: "/blog/addBlog",
                contentType : "application/json",           //将js中的对象,序列化成json字符串的形式进行传递,方便服务器进行处理
                data: JSON.stringify({
                    title: $("#title").val(),
                    content: $("#content").val()
                }),
                success : function (result) {
                    if(result.code == 200 || result.data == "success") {
                        location.replace("blog_list.html");
                    }else {
                        alert(result.errMessage);
                    }
                },
                error : function (err) {
                    if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                        location.replace("/blog_login.html");
                    }
                }
            })
        };
    </script>

服务器代码:

@PostMapping("/addBlog")
    public Result addBlog(@RequestBody BlogInfo blogInfo, HttpServletRequest request) {		//@RequestBody注解将客户端发送来的json转化为java对象
        try {
            //从token中获取当前用户信息id,储存到blog表中的userId中
            String token = request.getHeader("user_token");
            Integer userId = JWTUtils.getIdByToken(token);
            blogInfo.setUserId(userId);
            blogMapper.insertBlog(blogInfo);
            return Result.success("success");
        } catch (NullPointerException e) {
            log.error("发布失败:", e);
            return Result.fail("博客发布失败,请重新发布!");
        }
    }
显示、更改博客信息

显示博客信息调用上文显示博客详情信息同一个接口即可,更改博客信息等同于写博客接口,唯一区别在于服务器在对逻辑进行处理时,前者在数据库调用insert插入新的SQL,后者则是调用update进行更新。
在这里插入图片描述
客户端代码:

<script type="text/javascript">

        $(function () {                                     //文档编辑器官方提供的使用说明,下同
            var editor = editormd("editor", {
                width  : "100%",
                height : "550px",
                path: "blog-editormd/lib/"
            });
        });
        
        function submit() {
            $.ajax({                                //提交更新博客参数,成功返回博客列表页
                type: "post",
                url: "/blog/update",
                data: {
                    id: $("#blogId").val(),
                    title: $("#title").val(),
                    content: $("#content").val(),
                },
                success: function (result) {
                    if(result.code == 200 || result.data == "success"){
                        alert("更新成功");
                        location.replace("/blog_list.html");
                    }else {
                        alert(result.errMessage);
                    }
                },
                error : function (err) {
                    if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                        location.replace("/blog_login.html");
                    }
                }
            });
        }
        function getBlogInfo() {
            $.ajax({
                url: "blog/getBlogDetail" + location.search,            //同样调用getBlogDetail接口,传递当前博客id,收到服务器返回博客信息进行依次赋值
                type: "get",
                success: function (result) {
                    if (result.code == 200 && result.data != "") {
                        var blog = result.data;
                        $("#blogId").val(blog.id),
                        $("#title").val(blog.title);
                        editormd("editor", {
                            width: "100%",
                            height: "550px",
                            path: "blog-editormd/lib/",
                            onload: function () {
                                this.watch();
                                this.setMarkdown(blog.content);
                            }
                        });
                    }else {
                        alert(result.errMessage);
                    }
                },
                error : function (err) {
                    if(err.status == 401) {          //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
                        location.replace("/blog_login.html");
                    }
                }
            });
        }
        getBlogInfo();          //页面初始化时调用此函数,在编辑页面显示博客基本信息
    </script>

服务器代码:

@PostMapping("/update")
    public Result update(BlogInfo blogInfo) {
        try {
            blogMapper.updateBlog(blogInfo);
            return Result.success("success");
        } catch (NullPointerException e) {
            return Result.fail("更新失败!");
        }
    }

九、拦截器设置

避免用户使用时通过url直接跳过登录页面进入其他页面当中,或者知道服务器接口构造非法请求入侵,添加拦截器来指定用户只能通过特定url进行访问,增强了项目的安全性。

构建登录拦截器
/**
 * 构造登录拦截器,实现HandlerInterceptor接口,重写preHandle方法,刚放在作用于http请求后、解析请求逻辑前进行判断
 * 用户已经登录,token中存在用户信息,则返回ture,相反则为false
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            String token = request.getHeader("user_token");
            Claims claims = JWTUtils.parserJWT(token);
            return true;
        } catch (Exception e) {
            response.setStatus(401);
        }
        return false;
    }
}
配置拦截器
/**
 * 配置拦截器,实现WebMvcConfigurer接口,重写addInterceptors方法,注入配置登录拦截器依赖
 *将配置好的拦截器注入到列表中,添加拦截路径设置为所有,设置排除拦截路径,再次用List进行管理
 */
@Configuration
public class LoginConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginInterceptor)
               .addPathPatterns("/**")
               .excludePathPatterns(excludes);
    }

    private final List excludes = Arrays.asList(
            "/**/*.html",
            "/blog-editormd/**",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/user/login",
            "/user/register"
    );
}

在开发环境下对各种接口进行测试,没有问题即可打成jar包在服务器上运行。

十、注解解析

注解框架解析
@Slf4jLombok便于打印日志,简化日志记录
@DataLombok主要包含set、get、toString、equal等方法
@AutowiredSpring注入对象交给Spring管理,完成自动装配的工作
@RestControllerSpringController、ResponsBody结合体,返回值作为响应体
@GetMappingSpring处理HTTP的GET请求
@PostMappingSpring处理HTTP的POST请求
@RequestMappingSpring自动将返回内容转换为JSON格式,处理GE、POST请求
@ControllerAdviceSpring处理全局异常
@ComponentSpring通用的Spring管理Bean注解
@ConfigurationSpringSpring中标志配置类
总结:

博客管理系统主要在于数据库设计、自定义工具类、统一结果异常返回、接口前后端交互、拦截器处理几大关要素,前端设计因人而异每个人都有属于自己的审美,主要在于理解接口如何定义、怎样实现前后端交互做到方便、快捷、安全。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值