经典毕设项目-博客系统(spring boot、spring mvc、mybatis) gitee开源源码

目录

项目背景

核心技术

项目页面设计

注册页面

登录页面

博客列表页

博客详情页

个人博客列表页

个人博客发布页

个人博客修改页

项目模块与需求分析

AOP 处理模块

用户模块

文章模块

项目创建

实现 AOP 模块

实现登录拦截器

拦截器

拦截注册

实现统一数据返回数据格式

数据格式设定

实现统一数据返回的保底类

加盐加密、解密模块 

加密

加密思路

加密简图

解密

解密思路

实现用户模块

数据库设计

配置 MyBatis

创建实体类

创建 UserMapper

实现 UserMapper.xml

前后端交互——注册页面

客户端开发

服务器开发

前后端交互——登录页面

客户端开发

服务器开发

前后端交互——用户注销功能

客户端开发

服务器开发

实现文章模块

设计数据库

创建实体类

创建 ArticleMapper 接口

前后端交互——个人博客列表页

客户端开发

服务器开发

前后端交互——博客发布页面

客户端开发

服务器开发

前后端交互——个人博客修改页

客户端开发

服务器开发

前后端交互——文章删除功能(个人博客列表页)

客户端开发

服务器开发

前后端交互——博客详情页

客户端开发

服务器开发

前后端交互——博客列表页(分页功能)

客户端开发

服务器开发

小结


项目背景


实现一个网页版的博客系统。

支持以下核心功能:

  • 用户注册、登录、注销功能。
  • 已登录用户可对自己的文章进行发布、修改、查看详情、修改功能。
  • 未登录用户可以使用博客列表页,查看所有用户的文章详情。

核心技术


  • Spring / Spring Boot / Spring MVC
  • MySQL
  • MyBatis
  • HTML / CSS / JS / AJAX

项目页面设计


整个 Web 项目分为以下页面

  • 注册页面(任意用户可使用)
  • 登录页面(任意用户可使用)
  • 博客列表页(任意用户可使用)
  • 博客详情页(任意用户可使用)
  • 个人博客列表页(面向已登录用户)
  • 个人博客发布页(面向已登录用户)
  • 个人博客修改页(面向已登录用户)

注册页面

客户端输入账号和密码,增加用户,通过 MySQL 将用户信息进行持久化保存。

服务器通过 Spring + MyBatis 进行信息的增添。

登录页面

客户端输入用户名和密码,通过后端 Spring +  MyBatis 进行数据库的查询操作校验用户的账号和秘密是否正确,若正确则创建 Session 会话并跳转到博客列表页,若错误则用弹窗进行提示。

博客列表页

面向任意用户,都可以在该界面访问所有用发布的博客,并且可以点击 “查看全文” 查看博客详细信息。

服务器通过 Spring + MyBatis 进行数据库的查询操作,查询所有文章。

博客详情页

博客详情页可以分别从个人博客列表页和博客列表页打开,查看博客的详细信息。

服务器通过 Spring +  MyBatis 进行数据库的查询操作,查询当前访问的文章。

个人博客列表页

面向当前登录用户。该页面会展示当前登录用户的个人发布的所有博客,并可以对这些博客进行查看详情,修改,删除等功能。

服务器针对用户的以上操通过  Spring + MyBatis 进行相应的增删改查操作。

个人博客发布页

面向当前登录用户。该页面用户可以填写标题和正文,最后点击发布按钮进行发布博客。

服务器通过 MySQL 保存用户发布的博客信息。

个人博客修改页

面向当前登录用户。该页面可以进行对之前发布的页面进行编辑操作,修改完后点击发布博客即可修改文章。

服务器通过 Spring + MyBatis 对数据库进行相应的修改操作。

项目模块与需求分析


整个项目分为以下模块

  • AOP 处理模块
  • 加盐加密、解密模块
  • 用户模块
  • 文章模块

AOP 处理模块

AOP处理主要负责实现登录拦截器、统一返回数据格式处理。

登录拦截器:通过登录拦截器,拦截所有访问面向登录用户页面的非登录用户,保证用户隐私问题。

统一返回数据格式:统一 控制层(Controller)所有方法的返回类型,便于前端接收数据对格式的统一处理。

用户模块

主要负责用户的注册、登录、信息展示功能。

使用 MySQL 数据库存储数据。

客户端提供 登录页面 + 注册页面 + 个人博客列表页面 + 个人博客详情页面。

服务器使用 Session 会话保存用户登录信息和状态,基于 Spring Boot + Spring MVC + MyBatis 实现数据库的增删改查。

文章模块

主要负责管理文章的发布、查看详情、修改、删除功能。

使用 MySQL 数据库存储数据。

客户端提供 博客列表页 + 博客详情页 + 个人博客发布页 + 个人博客修改页。

服务器使用 Spring Boot + Spring MVC + MyBatis 实现数据库的增删改查。

项目创建


使用 IDEA 创建 Spring Boot 项目,引入以下依赖:

依赖就是常规的 Spring Boot / Spring MVC(Web) / MyBatis / Lombok 等,没啥特别的依赖~

	<dependencies>
		<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>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

实现 AOP 模块


实现登录拦截器

拦截器

创建 config.LoginInterceptor 类,实现 HandlerInterceptor 接口,重写 preHandle 方法。

通过 Session 会话信息检验用户是否登录,没登陆则跳转到 login.html 页面

import com.example.demo.common.AppVariable;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 登录拦截校验
 */
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 检验用户是否登录,没登陆则跳转到 login.html 页面
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //检验 session 是否为空
        HttpSession session = request.getSession(false);
        if(session == null || session.getAttribute(AppVariable.USER_SESSION_KEY) == null) {
            response.sendRedirect("/login.html");
            return false;
        }
        return true;
    }
}

拦截注册

创建 config.AppConfig 类实现 WebMvcConfigurer 接口,重写 addInterceptors 方法。

用来注册添加刚刚写的拦截器。

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 登录拦截校验的保底类
 */
@ControllerAdvice
public class AppConfig implements WebMvcConfigurer {

    /**
     * 注册拦截
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/editor.md/**")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/user/show-aut")
                .excludePathPatterns("/art/incr-rcount")
                .excludePathPatterns("/art/detail")
                .excludePathPatterns("/art/listbypage")
                .excludePathPatterns("/art/allcount")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/blog_list.html")
                .excludePathPatterns("/blog_content.html");
    }
}

实现统一数据返回数据格式

数据格式设定

创建一个通用类 common.AjaxResult。

通过这个类来提供统一返回的数据格式,大体上分为两种方法,分别是返回操作成功、失败的结果,具体的返回的数据格式中包含状态码、信息描述、数据信息。

import java.io.Serializable;

/**
 * 统一数据格式返回
 */
@Data
public class AjaxResult implements Serializable {

    //状态码
    private Integer code;
    //状态码描述信息
    private String msg;
    //返回的数据
    private Object data;

    /**
     * 操作成功返回的结果
     */
    public static AjaxResult success(Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(200);
        result.setMsg("");
        result.setData(data);
        return result;
    }

    public static AjaxResult success(int code, Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg("");
        result.setData(data);
        return result;
    }

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

    /**
     * 返回失败结果
     */
    public static AjaxResult fail(int code, String msg) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }

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

实现统一数据返回的保底类

创建 config.ResponseAdvice 继承 ResponseBodyAdvice 接口,重写 supports、beforeBodyWrite方法。

这个类用来在返回数据之前,检测数据类型是否为统一对象,如果不是就封装为统一数据格式,如果是,就直接返回。

import com.example.demo.common.AjaxResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 实现统一数据返回的保底类
 * 说明:在返回数据之前,检测数据类型是否为统一对象,如果不是就封装
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 开关
     * @param returnType
     * @param converterType
     * @return
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 对数据格式进行校验和封装
     * @param body
     * @param returnType
     * @param selectedContentType
     * @param selectedConverterType
     * @param request
     * @param response
     * @return
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(body instanceof AjaxResult) {
            return body;
        }
        //String比较特殊,要单独处理
        if(body instanceof String) {
            return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);
    }
}

加盐加密、解密模块 


加密

加密思路

加密的主要有以下几步:

  1. 产生随机盐值(32位)。解释:这里可以使用 UUID 中的 randomUUID 方法生成一个长度为 36 位的随机盐值(格式为xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)),而我们约定的格式中(完全由自己定义)不需要"-",因此将产生的随机盐值先使用 toString 方法转化成字符串,最后用 replaceAll 方法将字符串中的 "-" 用空字符串代替即可。
  2. 将盐值和明文拼接起来,然后整体使用 md5 加密,得到密文(32位)。解释:这里就是对明文进行加密的过程。
  3. 根据自己约定的格式,使用 “32位盐值 +&+ 32位加盐后得到的密文” 方式进行加密得到最终密码。解释:我们这样去约定格式,是为了更容易的得到盐值(通过 split 以 $ 为分割符进行分割,得到的字符串数组下标 0 的就是我们需要的盐值),让我们能够解密。

Ps:UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。UUID的唯一缺陷在于生成的结果串会比较长。

加密简图

创建 common.PasswordUtils 类。

    /**
     * 加盐并生成密码
     * @param password 用户输入的明文密码
     * @return 保存到数据库中的密码
     */
    public static String encrypt(String password) {
        // 产生盐值(32位)  这里要注意去掉 UUID 生成 -
        String salt = UUID.randomUUID().toString().replaceAll("-", "");
        // 生成加盐之后的密码((盐值 + 密码)整体 md5 加密)
        String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 生成最终密码(保存到数据库中的密码)[自己约定的格式:32位盐值 +&+ 32位加盐后的密码]
        // 这样约定格式是为了解密的时候方便得到盐值
        String finalPassword = salt + "$" + saltPassword;
        return finalPassword;
    }

解密

解密思路

解密的一个大体思路如下:

  1. 用户输入的密码使用刚刚讲到的加密思路再进行加密一次。解释:这里我们重载上面讲到的加密方法,需要传入两个参数,分别是用户输入的密码和盐值(这里的盐值我们可以通过对数据库中的最终加密密码进行字符串分割 $ 符号得到),然后对用户信息进行加密。
  2. 将刚刚加密的密码 对比 数据库保存的用户加密的最终密码 是否一致即可。

通过上述思路可以知道,实习解密思路需要两个方法~


    /**
     * 加盐生成密码(方法1的重载)
     * 此方法在验证密码的适合需要(将用户输入的密码使用同样的盐值加密后对比)
     * @param password 明文
     * @param salt 固定的盐值
     * @return 最终密码
     */
    public static String encrypt(String password, String salt) {
        // 生成加盐后的密码
        String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 生成最终密码(约定格式: 32位 盐值 +&+ 32位加盐后的密码)
        String finalPassword = salt + "$" + saltPassword;
        return finalPassword;
    }

    /**
     * 验证密码
     * @param inputPassword 用户输入明文密码
     * @param finalPassword 数据库中保存的最终密码
     * @return
     */
    public static boolean check(String inputPassword, String finalPassword) {
        // 判空处理
        if(StringUtils.hasLength(inputPassword) && StringUtils.hasLength(finalPassword) &&
        finalPassword.length() == 65) {
            // 得到盐值(之前约定: $前面的就是盐值)
            String salt = finalPassword.split("\\$")[0];// 由于 $ 在这里也可以表示通配符,所以需要使用 \\ 进行转义
            // 使用之前的加密步骤将明文进行加密,生成最终密码
            String confirmPassword = PasswordUtils.encrypt(inputPassword, salt);
            // 对比两个最终密码是否相同
            return confirmPassword.equals(finalPassword);
        }
        return false;
    }

实现用户模块


数据库设计

创建 userinfo 表示用户信息和状态(以下为总表)

-- 创建数据库
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 timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    `state` int default 1
) default charset 'utf8mb4';

-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';

-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
  	vid int primary key,
  	`title` varchar(250),
  	`url` varchar(1000),
		createtime timestamp default current_timestamp,
		updatetime timestamp default current_timestamp,
  	uid int
)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)
    values('Java','Java正文',1);

-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);

-- 后续修改
-- 用户名不可以重复
alter table userinfo add unique(username);

-- 修改用户密码字长
alter table userinfo modify password varchar(65);

配置 MyBatis

编辑 application.yml

# mysql connection
spring:
  jackon:
    date-format: yyyy-MM-dd
    time-zone: GMT+8
  datasource:
    url: jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false
    username: root
    password: 1111
    driver-class-name: com.mysql.cj.jdbc.Driver
  # mybatis xml save path
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# print log
logging:
  level:
    com:
      example:
        demo: debug

创建实体类

创建 entity.UserInfo 类

@Data
public class UserInfo {

    private Integer id;
    private String username;
    private String password;
    private String photo;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private Integer state;

}

创建 UserMapper

创建 mapper.UserMapper 接口。

主要提供以下几个方法:

  • reg:用户注册
  • getUserByName:根据用户名获取用户信息,用于登录页面实现登录功能。
  • getUserById:根据用户 id 查询用户信息,用于用户信息卡片上展示用户信息。
import com.example.demo.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserMapper {

    /**
     * 用户注册
     * @return
     */
    Integer reg(UserInfo userInfo);

    /**
     * 根据用户名获取用户信息
     */

    UserInfo getUserByName(@Param("username") String username);

    /**
     * 根据用户 id 查询用户信息
     * @param id
     * @return
     */
    UserInfo getUserById(@Param("id") Integer id);

}

实现 UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">

    <insert id="reg">
        insert into userinfo(username, password)
        values(#{username}, #{password})
    </insert>

    <select id="getUserByName" resultType="com.example.demo.entity.UserInfo">
        select * from userinfo where username = #{username}
    </select>

    <select id="getUserById" resultType="com.example.demo.entity.UserInfo">
        select * from userinfo where id = #{id};
    </select>

</mapper>

前后端交互——注册页面

客户端开发

创建 reg.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册页面</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/login.css">
    <script src="js/jquery.min.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="login.html">登陆</a>
        <!-- <a href="#">注销</a> -->
    </div>
    <!-- 版心 -->
    <div class="login-container">
        <!-- 中间的注册框 -->
        <div class="login-dialog">
            <h3>注册</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" id="username">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" id="password">
            </div>
            <div class="row">
                <span>确认密码</span>
                <input type="password" id="password2">
            </div>
            <div class="row">
                <button id="submit" onclick="mysub()">提交</button>
            </div>
        </div>
    </div>

</body>
</html>

在 reg.html中编写 js 代码

通过 jQuery 中的 AJAX 和服务器进行交互。

    <script>
        //提交注册事件
        function mysub() {
            //1.进行非空校验
            var username = jQuery("#username");
            var password = jQuery("#password");
            var password2 = jQuery("#password2");
            if(username.val() == "") {
                alert("请先输入用户名!");
                username.focus();
                return;
            }
            if(password.val() == "") {
                alert("请先输入密码!");
                password.focus();
                return;
            }
            if(password2.val() == "") {
                alert("请先输入确认密码!");
                password2.focus();
                return;
            }
            //2.判断两次是否一致
            if(password.val() != password2.val()) {
                alert("两次输入的密码不一致,请检查!")
                return;
            }
            //3.ajax 提交请求
            jQuery.ajax({
                url: '/user/reg',
                type: 'POST',
                data: {
                    "username": username.val(),
                    "password": password.val()
                },
                success: function (result) {
                    if(result != null && result.code == 200 && result.data == 1) {
                        //执行成功
                        if(confirm("恭喜:注册成功! 是否要跳转到登录页面?")) {
                            location.href = '/login.html';
                        }
                    } else {
                        alert("很抱歉执行失败,请稍后再试");
                    }
                }
            });
        }

    </script>

服务器开发

创建 controller.UserController 类。

首先检测请求中参数的有效性,接着对密码进行加盐处理,最后操作数据库添加用户信息,最后向前端相应成功信息。

主要实现以下方法:

    @RequestMapping("/reg")
    public AjaxResult reg(UserInfo userInfo) throws IOException {
        //非法参数的校验
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) ||
        !StringUtils.hasLength(userInfo.getPassword())) {
            return AjaxResult.fail(403, "非法参数");
        }
        userInfo.setPassword(PasswordUtils.encrypt(userInfo.getPassword()));
        return AjaxResult.success(userService.reg(userInfo));
    }

前后端交互——登录页面

客户端开发

创建 login.html。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登陆页面</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/login.css">
    <script src="js/jquery.min.js"></script>

</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="reg.html">注册</a>
        <!-- <a href="#">注销</a> -->
    </div>
    <!-- 版心 -->
    <div class="login-container">
        <!-- 中间的登陆框 -->
        <div class="login-dialog">
            <h3>登陆</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" id="username">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" id="password">
            </div>
            <div class="row">
                <button id="submit" onclick="login()">提交</button>
            </div>
        </div>
    </div>



</body>
</html>

编写 js 代码。

通过 jQuery 接收定位元素,获取用户输入。

首先检验账号密码的有效性,接着向服务器发送 ajax 请求,请求中包含数据为:“账号、密码”。

    <script>

        function login() {
            //非空校验
            var username = jQuery("#username");
            var password = jQuery("#password");
            if(username.val() == "") {
                alert("请先输入用户名!");
                username.focus();
                return;
            }
            if(password.val() == "") {
                alert("请先输入密码!");
                password.focus();
                return;
            }
            
            //ajax 登录接口
            jQuery.ajax({
                type: "POST",
                url: "/user/login",
                data: {
                    "username": username.val(),
                    "password": password.val()
                },
                success: function(result) {
                    if(result != null && result.code == 200 && result.data != null) {
                        //登录成功
                        location.href = '/myblog_list.html';
                    } else {
                        alert("很抱歉登陆失败,用户名或密码错误!");
                    }
                }
            });
        }

    </script>

服务器开发

收到请求后,先检验前端传入参数的有效性,接着根据 用户名 查询数据库中对应的用户信息,随后拿到用户的加盐密码,通过 PasswordUtils 提供的 check 方法检验用户输入的密码是否正确,如果校验成功,则创建 Session 会话,保存当前登录用户信息,最后返回成功信息。

    @RequestMapping("/login")
    public AjaxResult login(HttpServletRequest request, String username, String password) {
        //1.非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return AjaxResult.fail(403, "非法请求");
        }
        //2.查询数据库
        UserInfo userInfo = userService.getUserByName(username);
        //ps:这里注意要对加盐密码解密
        if(userInfo == null || !PasswordUtils.check(password, userInfo.getPassword())) {
            return AjaxResult.fail(403, "参数错误");
        }
        //3.登录成功
        HttpSession session = request.getSession(true);
        session.setAttribute(AppVariable.USER_SESSION_KEY, userInfo);
        return AjaxResult.success(1);
    }

前后端交互——用户注销功能

客户端开发

编写 js 代码。

点击注销钮触发ajax注销请求,请求成功后跳转到登录页面。

//注销登录
function logout() {
    if(confirm("确定注销?")) {
        jQuery.ajax({
            type: 'POST',
            url: '/user/logout',
            data: {},
            success: function(result) {
                if(result != null && result.code == 200) {
                    //注销成功,重定向到主页
                    location.assign("login.html");
                } else {
                    alert("很抱歉!注销失败,请稍后重试!");
                }
            }
        });
    }
}

服务器开发

首先获取 session 会话并检验有效性,然后删除 session 信息。

    @RequestMapping("/logout")
    public AjaxResult logout(HttpSession session) {
        //这里是直接拿到 session
        if(session != null || session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {
            //用户信息校验正确,接下来开始删除会话
            session.removeAttribute(AppVariable.USER_SESSION_KEY);
            return AjaxResult.success(1);
        }
        //注销失败
        return AjaxResult.fail(403, "session 信息错误!");
    }

实现文章模块


设计数据库

创建 articleinfo 表,用来存储文章信息。

-- 创建数据库
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 timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    `state` int default 1
) default charset 'utf8mb4';

-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';

-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
  	vid int primary key,
  	`title` varchar(250),
  	`url` varchar(1000),
		createtime timestamp default current_timestamp,
		updatetime timestamp default current_timestamp,
  	uid int
)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)
    values('Java','Java正文',1);

-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);

-- 后续修改
-- 用户名不可以重复
alter table userinfo add unique(username);

-- 修改用户密码字长
alter table userinfo modify password varchar(65);

创建实体类

注意要对时间进行特殊处理,否则返回给前端的结果就是一个时间戳。

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 文章表
 */
@Data
public class ArticleInfo {

    private Integer id;
    private String title;
    private String content;

    //格式化的时间处理
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDateTime createtime;
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDateTime updatetime;

    private Integer uid;
    private Integer rcount;
    private Integer state;

}

创建 ArticleMapper 接口

主要提供以下几个方法:

  • getArtCountByUid:根据用户 id 获取该用户的文章总数,主要用于用户信息模块中的文章总数设置。
  • getAllArtInfoByUid:根据用户 id 获取该用户的所有文章,主要用于个人博客列表页获取当前登录用户的所有博客。
  • delArt:通过文章 id 和用户 id 进行删除文章(若文章 id 对应的文章 uid 无法对应,则无法删除博客)
  • getArtInfoById:根据文章 id 获取文章的详情信息,主要用于文章详情页获取文章信息。
  • incrRCount:增加阅读量,用于博客详情页对阅读量的增加。
  • add:发布博客,主要用于博客发布页。
  • update:修改博客,主要用于博客修改页。
  • getListByPage:博客列表页分页功能,通过每页文章显示最大个数和 offset 偏移量来进行分页。
  • getArtAllCount:获取文章总数,用于分页功能中的点击末页跳转到最后一页功能。
import com.example.demo.entity.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
@Repository
@Mapper
public interface ArticleMapper {

    /**
     * 根据用户 id 获取该用户的文章总数
     * @param uid
     * @return
     */
    Integer getArtCountByUid(@Param("uid") Integer uid);

    /**
     * 根据用户 id 获取该用户的所有文章
     * @return
     */
    List<ArticleInfo> getAllArtInfoByUid(@Param("uid") Integer uid);

    /**
     * 通过文章 id 和用户 id 进行删除文章(若文章 id 对应的文章 uid 无法对应,则无法删除博客)
     * @param id
     * @param uid
     * @return
     */
    Integer delArt(@Param("id") Integer id,
                   @Param("uid") Integer uid);

    /**
     * 根据文章 id 获取文章的详情信息
     * @param id
     * @return
     */
    ArticleInfo getArtInfoById(@Param("id") Integer id);

    /**
     * 阅读量 +1
     * @param id
     * @return
     */
    Integer incrRCount(@Param("id") Integer id);

    /**
     * 发布博客
     * @param articleInfo
     * @return
     */
    Integer add(ArticleInfo articleInfo);

    /**
     * 修改文章
     * @param articleInfo
     * @return
     */
    Integer update(ArticleInfo articleInfo);

    /**
     * 博客列表页分页功能
     * @param psize 每页显示的文章个数
     * @param offset 文章下标(偏移量)
     * @return
     */
    List<ArticleInfo> getListByPage(@Param("psize") Integer psize,
                                    @Param("offset") Integer offset);

    /**
     * 获取文章总数
     * @return
     */
    Integer getArtAllCount();
}

前后端交互——个人博客列表页

客户端开发

创建 myblog_list.html。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>博客列表</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_list.css">
    <script src="js/jquery.min.js"></script>
    <script src="js/common.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_add.html">写博客</a>
        <a href="javascript:logout()">注销</a>
        <!-- <a href="#">注销</a> -->
    </div>
    <!-- 版心 -->
    <div class="container">
        <!-- 左侧个人信息 -->
        <div class="container-left">
            <div class="card">
                <img src="img/doge.jpg" class="avtar" alt="">
                <h3 id="username"></h3>
                <a href="http:www.github.com">github 地址</a>
                <div class="counter">
                    <span>文章</span>
                    <span>分类</span>
                </div>
                <div class="counter">
                    <span id="artCount"></span>
                    <span>1</span>
                </div>
            </div>
        </div>
        <!-- 右侧内容详情 -->
        <div id="artDiv" class="container-right">

        </div>
    </div>



</body>
</html>

编写 js 代码

主要涉及到两个 HTTP 请求,分别是获取用户信息、获取文章列表信息.

    <script>
        //获取用户信息
        function getUserInfo() {
            var username = jQuery('#username');
            var artCount = jQuery('#artCount');
            jQuery.ajax({
                type: 'GET',
                url: '/user/showinfo',
                data: {},
                success: function (result) {
                    if (result != null && result.code == 200 && result.data != null) {
                        //用户信息获取成功
                        username.text(result.data.username);
                        artCount.text(result.data.artCount);
                    } else {
                        alert("用户信息获取失败");
                    }
                }
            });
        }
        getUserInfo();

        //获取文章列表数据
        function getMyArtList() {
            jQuery.ajax({
                type: 'POST',
                url: '/art/mylist',
                data: {},
                success(result) {
                    if (result != null || result.code == 200) {
                        //有两种情况,一种是发表了文章,一种是没有发表文章
                        if (result.data != null && result.data.length > 0) {
                            //用户发表了文章
                            var artListDiv = "";
                            for (var i = 0; i < result.data.length; i++) {
                                //拿到每一篇博客
                                var artItem = result.data[i];
                                artListDiv += '<div class="blog">';
                                artListDiv += '<div class="title">' + artItem.title + '</div>';
                                artListDiv += '<div class="date">' + "发布时间:" + artItem.createtime + '</div>';
                                artListDiv += '<div class="desc">';
                                artListDiv += artItem.content;
                                artListDiv += '</div>';
                                artListDiv += '<a href="blog_content.html?id=' + artItem.id + '" class="detail">查看全文 &gt;&gt;</a>';
                                artListDiv += '<a href="blog_edit.html?id=' + artItem.id + '" class="detail">修改文章 &gt;&gt;</a>';
                                artListDiv += '<a href="javascript:delArt(' + artItem.id + ')" class="detail">删除文章 &gt;&gt;</a>';
                                artListDiv += '</div>';
                            }
                            //将 html 填充进去
                            jQuery("#artDiv").html(artListDiv);
                        } else {
                            //用户未发表过文章
                            jQuery("#artDiv").html("<h3>暂无文章 <h3>");
                        }
                    } else {
                        alert("文章查询出错,请稍后重试!");
                    }
                }
            });
        }
        getMyArtList();




    </script>

服务器开发

分别调用不同的接口使用 MyBatis 进行数据库进行查询用户信息和文章信息的操作。

    @RequestMapping("/mylist")
    public AjaxResult getArtListById(HttpServletRequest request) {
        //获取用户信息
        UserInfo userInfo = UserSessionUtils.getUser(request);
        if(userInfo != null) {
            //成功获取用户信息,需要使用用户 id 获取该用户的所有文章
            return AjaxResult.success(articleService.getAllArtInfoByUid(userInfo.getId()));
        }
        return AjaxResult.fail(403, "参数错误");
    }

    @RequestMapping("/detail")
    public AjaxResult getArtInfoById(Integer id) {
        if(id == null || id <= 0) {
            return AjaxResult.fail(403, "参数非法!");
        }
        //获取文章信息
        ArticleInfo articleInfo = articleService.getArtInfoById(id);
        if(articleInfo == null) {
            return AjaxResult.fail(403, "参数非法");
        }
        return AjaxResult.success(articleInfo);
    }

前后端交互——博客发布页面

客户端开发

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>添加博客</title>

    <!-- 引入自己写的样式 -->
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_edit.css">
    <!-- 公共 js -->
    <script src="js/jquery.min.js"></script>
    <script src="js/common.js"></script>

    <!-- 引入 editor.md 的依赖 -->
    <link rel="stylesheet" href="editor.md/css/editormd.min.css" />
    <script src="editor.md/editormd.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="javascript:logout()">注销</a>
    </div>
    <!-- 编辑框容器 -->
    <div class="blog-edit-container">
        <!-- 标题编辑区 -->
        <div class="title">
            <input type="text" id="title" placeholder="在这里写下文章标题">
            <button onclick="mysub()">发布文章</button>
        </div>
        <!-- 创建编辑器标签 -->
        <div id="editorDiv">
            <textarea id="editor-markdown" style="display:none;"></textarea>
        </div>
    </div>


</body>

</html>

编写 js 代码

这里来需要引入第三方库的 markdown 编辑器,点击发布博客触发 ajax 请求,将文章信息发送给服务器。

    <script>
        var editor;
        function initEdit(md){
            // 编辑器设置
            editor = editormd("editorDiv", {
                // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. 
                width: "100%",
                // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
                height: "calc(100% - 50px)",
                // 编辑器中的初始内容
                markdown: md,
                // 指定 editor.md 依赖的插件路径
                path: "editor.md/lib/",
                saveHTMLToTextarea: true // 
            });
        }
        initEdit("# 在这里写下一篇博客"); // 初始化编译器的值
        // 提交
        function mysub(){
            if(confirm("确认提交?")) {
                // 1.非空校验
                var title = jQuery('#title');
                if(title.val() == "") {
                    alert("请先输入标题!");
                    title.focus();
                    return;
                }
                if(editor.getValue() == "") {
                    alert("请先输入文章内容!");
                    return;
                }
                // 2.请求后端进行添加操作
                jQuery.ajax({
                    type: 'POST',
                    url: '/art/add',
                    data: {
                        "title": title.val(),
                        "content": editor.getValue()
                    },
                    success: function(result) {
                        if(result != null && result.code == 200 && result.data == 1) {
                            if(confirm("恭喜:文章添加成功!是否继续添加文章?")) {
                                //刷新当前页面
                                location.href = location.href;
                            } else {
                                location.href = "myblog_list.html";
                            }
                        } else {
                            alert("抱歉:文章添加失败,请重试!");
                        }
                    }
                });
                // alert(editor.getValue()); // 获取值
            }
        }


    </script>

服务器开发

首先对前端传入的参数(文章标题、正文)进行非空校验,然后通过 获取 Session 信息获取用户的 id 属性,设置 id 属性,返回成功相应。

    @RequestMapping("/add")
    public AjaxResult add(HttpServletRequest request, ArticleInfo articleInfo) {
        //1.非空校验
        if(articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle()) ||
        !StringUtils.hasLength(articleInfo.getContent())) {
            return AjaxResult.fail(403, "非法参数");
        }
        //2.得到当前登录用户(uid)
        UserInfo userInfo = UserSessionUtils.getUser(request);
        if(userInfo == null || userInfo.getId() < 0) {
            return AjaxResult.fail(403, "参数非法!");
        }
        //3.添加数据库并返回结果
        articleInfo.setUid(userInfo.getId());
        return AjaxResult.success(articleService.add(articleInfo));
    }

前后端交互——个人博客修改页

客户端开发

创建 blog_edit.html.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>博客修改</title>

    <!-- 引入自己写的样式 -->
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_edit.css">

    <!-- 引入 editor.md 的依赖 -->
    <link rel="stylesheet" href="editor.md/css/editormd.min.css" />
    <script src="js/jquery.min.js"></script>
    <script src="editor.md/editormd.js"></script>
    <script src="js/common.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="javascript:logout()">注销</a>
    </div>
    <!-- 编辑框容器 -->
    <div class="blog-edit-container">
        <!-- 标题编辑区 -->
        <div class="title">
            <input type="text" id="title">
            <button onclick="mysub()">发布文章</button>
        </div>
        <!-- 创建编辑器标签 -->
        <div id="editorDiv">
            <textarea id="editor-markdown" style="display:none;"></textarea>
        </div>
    </div>


</body>

</html>

编写 js 代码 

客户端 url 获取文章 id ,再将文章 id 和用户修改的文章标题和内容通过 ajax 请求发送给服务器,服务器若修改成功,就跳转到个人博客列表页。

    <script>
        var editor;
        function initEdit(md) {
            // 编辑器设置
            editor = editormd("editorDiv", {
                // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. 
                width: "100%",
                // 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度
                height: "calc(100% - 50px)",
                // 编辑器中的初始内容
                markdown: md,
                // 指定 editor.md 依赖的插件路径
                path: "editor.md/lib/",
                saveHTMLToTextarea: true // 
            });
        }
        // initEdit("# 在这里写下一篇博客"); // 初始化编译器的值


            //获取 当前文章 id 并进行非空校验
            var id = getUrlValue("id");
            if(id == "") {
                alert("参数非法,请重试!");
                location.assign('/myblog_list.html');
            }

        // 提交
        function mysub() {
            if(confirm("确定修改文章?")) {
                // 1.判空处理
                var title = jQuery('#title');
                if (title.val() == "") {
                    alert("标题不能为空!");
                }
                var content = jQuery('#editor-markdown');
                if (content.val() == "") {
                    alert("正文不能为空!");
                }

                // 2.后端修改文章
                jQuery.ajax({
                    type: 'POST',
                    url: '/art/update',
                    data: {
                        "id": id,
                        "title": jQuery('#title').val(),
                        "content": editor.getValue()
                    },
                    success: function(result) {
                        if(result != null && result.code == 200 && result.data == 1) {
                            alert("恭喜,修改成功!");
                            location.assign('myblog_list.html');
                        } else {
                            alert("文章修改失败,请稍后重试!");
                        }
                    }
                });
            }
            // alert(editor.getValue()); // 获取值
            // editor.setValue("#123") // 设置值
        }



        //初始化 markdown
        function initMarkdown() {
            //初始化
            jQuery.ajax({
                type: 'POST',
                url: '/art/detail',
                data: { "id": id },
                success: function (result) {
                    if (result != null && result.code == 200 && result.data.id > 0) {
                        jQuery("#title").val(result.data.title);
                        initEdit(result.data.content);
                    } else {
                        alert("文章查询失败,请稍后重试!");
                    }
                }
            });
        }
        initMarkdown();


    </script>

服务器开发

首先对文章信息进行非空校验,再通过 session 信息获取用户 id ,这里的用户 id 就是修改文章的关键,因为只有 用户表中的 id 和 和文章表中的 uid 匹配的上才进行文章的修改,防止其他用户在前端通过非法手段修改他人文章(例如使用 postman 发送非法请求,篡改他人信息),最后通过 MyBatis 进行数据库的修改操作。

    @RequestMapping("/update")
    public AjaxResult update(HttpServletRequest request, ArticleInfo articleInfo) {
        //非空校验
        if(articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle()) ||
                !StringUtils.hasLength(articleInfo.getContent())){
            return AjaxResult.fail(403, "参数非法!");
        }
        //获取用户信息,防止其他用户修改另一个用户的文章
        UserInfo userInfo = UserSessionUtils.getUser(request);
        if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) ||
                userInfo.getId() <= 0) {
            return AjaxResult.fail(403, "参数非法");
        }
        //校验是否未当前用户的关键信息
        articleInfo.setUid(userInfo.getId());
        articleInfo.setUpdatetime(LocalDateTime.now());
        return AjaxResult.success(200, articleService.update(articleInfo));
    }

前后端交互——文章删除功能(个人博客列表页)

客户端开发

编写 js 代码

客户端获取文章 id, 发送 ajax 请求给后端,请求的数据中带有文章 id ,方便后端通过文章 id 进行文章的删除.

        //删除文章
        function delArt(id) {
            if(confirm("确定删除该文章?")) {
                jQuery.ajax({
                    type: 'POST',
                    url: '/art/del',
                    data: {"id": id}, //删除文章的根据
                    success: function(result) {
                        if(result != null && result.code == 200 && result.data == 1) {
                            //文章删除成功
                            alert("恭喜你,文章删除成功!");
                            //刷新页面
                            location.href = location.href;
                        } else {
                            alert("文章删除失败,请稍后再试!");
                        }
                    }
                });
            }
        }

服务器开发

首先对 id 进行非空校验(防止前端通过 postman 等工具发送非法请求),接着通过 session 获取用户信息,最后使用 mapper 接口通过 文章 id 进行数据库文章信息的删除操作。

    @RequestMapping("/del")
    public AjaxResult delArt(Integer id, HttpServletRequest request) {
        if(id == null) {
            return AjaxResult.fail(403, "参数有误");
        }
        UserInfo userInfo = UserSessionUtils.getUser(request);
        if(userInfo == null) {
            return AjaxResult.fail(403, "session参数错误!");
        }
        return AjaxResult.success(articleService.delArt(id, userInfo.getId()));
    }

前后端交互——博客详情页

客户端开发

创建 blog_content.html 页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>博客正文</title>
    <link rel="stylesheet" href="css/conmmon.css">
    <link rel="stylesheet" href="css/blog_content.css">
    <link rel="stylesheet" href="editor.md/css/editormd.preview.min.css" />
    <script src="js/jquery.min.js"></script>
    <script src="editor.md/editormd.js"></script>
    <script src="editor.md/lib/marked.min.js"></script>
    <script src="editor.md/lib/prettify.min.js"></script>
    <script src="js/common.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="blog_list.html">主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="javascript:logout()">注销</a>
    </div>
    <!-- 版心 -->
    <div class="container">
        <!-- 左侧个人信息 -->
        <div class="container-left">
            <div class="card">
                <img src="img/doge.jpg" class="avtar" alt="">
                <h3 id="username"></h3>
                <a href="http:www.github.com">github 地址</a>
                <div class="counter">
                    <span>文章</span>
                    <span>分类</span>
                </div>
                <div class="counter">
                    <span id="artCount">2</span>
                    <span>1</span>
                </div>
            </div>
        </div>
        <!-- 右侧内容详情 -->
        <div class="container-right">
            <div class="blog-content">
                <!-- 博客标题 -->
                <h3 id="title"></h3>
                <!-- 博客时间 -->
                <div class="date">修改时间:<span id="updatetime"></span> &nbsp;&nbsp; 阅读量:<span id="rcount"></span>
                </div>
                <!-- 博客正文 -->
                <div id="editorDiv">

                </div>
            </div>
        </div>
    </div>

</body>

</html>

编写 js 代码

这里涉及三个 HTTP 请求:

  • 获取文章信息
  • 获取用户信息
  • 修改文章阅读量信息

获取文章和用户信息:首先客户端获取 url 中的文章 id,并将文章 id 作为请求参数发送给服务器,进而得到对应的文章信息,将文章信息进行展示,再将获取到的文章信息中的 uid 作为请求参数,继续向服务器发送 HTTP 请求,获取用户信息。

修改文章阅读量信息:首先客户端获取 url 中的文章 id 并进行校验,再将 id 作为 ajax 请求的参数发送给服务器,服务器再进行修改数据库阅读量 +1 的操作。

    <script type="text/javascript">
            var editormd;
            function initEdit(md){
                editormd = editormd.markdownToHTML("editorDiv", {
                markdown : md, // Also, you can dynamic set Markdown text
                // htmlDecode : true,  // Enable / disable HTML tag encode.
                // htmlDecode : "style,script,iframe",  // Note: If enabled, you should filter some dangerous HTML tags for website security.
                });
            }



        //查询文章详情
        function getArtDetail(id) {
            if(id == "") {
                alert("非法参数!");
                return;
            }
            jQuery.ajax({
                type: 'POST',
                url: '/art/detail',
                data: {"id": id},
                success: function(result) {
                    if(result != null && result.code == 200 && result.data.id > 0) {
                        //result.data.id > 0 这一条判断是必要的,因为有可能客户端通过 postman 发送请求,状态状态码是没问题的(状态码没问题只能表示交互没问题,但不表示里面一定会有数据)
                        jQuery("#title").html(result.data.title);
                        jQuery("#updatetime").html(result.data.updatetime);
                        jQuery("#rcount").html(result.data.rcount);
                        initEdit(result.data.content);
                        //将用户 id 作为参数,获取用户信息
                        showAuthor(result.data.uid);
                    } else {
                        alert("文章查询失败,请稍后重试!");
                    }
                }
            });
        }
        getArtDetail(getUrlValue("id"));

        //获取文章作者信息
        function showAuthor(uid) {
            jQuery.ajax({
                type: 'POST',
                url: '/user/show-aut',
                data: {"uid": uid},
                success: function(result) {
                    if(result != null && result.code == 200) {
                        //作者信息获取成功(这里使用 text 和 html 效果都一样)
                        jQuery("#username").text(result.data.username);
                        jQuery("#artCount").text(result.data.artCount);
                    } else {
                        alert("作者信息获取失败!");
                    }
                }
            });
        }

        //阅读量 + 1
        function updataRCount() {
            //先得到文章的 id
            var id = getUrlValue('id');
            if(id != "" && id > 0) {
                jQuery.ajax({
                    type: 'POST',
                    url: '/art/incr-rcount',
                    data: {"id": id},
                    success: function(result) {}
                });
            }
        }
        updataRCount();
            
    </script> 

服务器开发

这里服务器根据不同的请求,调用不同的 mapper 接口使用 MyBatis 进行数据库信息的修改操作。

前端请求用户信息,后端的响应代码在用户模块那里展示过了,这里不多做展示。

    @RequestMapping("/detail")
    public AjaxResult getArtInfoById(Integer id) {
        if(id == null || id <= 0) {
            return AjaxResult.fail(403, "参数非法!");
        }
        //获取文章信息
        ArticleInfo articleInfo = articleService.getArtInfoById(id);
        if(articleInfo == null) {
            return AjaxResult.fail(403, "参数非法");
        }
        return AjaxResult.success(articleInfo);
    }

    @RequestMapping("incr-rcount")
    public AjaxResult incrRCount(Integer id) {
        if(id != null && id > 0) {
            return AjaxResult.success(articleService.incrRCount(id));
        }
        return AjaxResult.fail(403, "未知错误!");
    }

前后端交互——博客列表页(分页功能)

客户端开发

创建 blog_list.html 页面。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>博客列表</title>
    <link rel="stylesheet" href="css/list.css">
    <link rel="stylesheet" href="css/blog_list.css">
    <style>
        .nav {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            height: 50px;
        }

        .container {
            padding-top: 80px;
            height: auto;
        }

        .container-right {
            width: auto;
        }

        .blog-pagnation-wrapper {
            height: 40px;
            margin: 16px 0;
            text-align: center;
        }

        .blog-pagnation-item {
            display: inline-block;
            padding: 8px;
            border: 1px solid #d0d0d5;
            color: #333;
        }

        .blog-pagnation-item:hover {
            background: #4e4eeb;
            color: #fff;
        }

        .blog-pagnation-item.actvie {
            background: #4e4eeb;
            color: #fff;
        }
    </style>
    <script src="js/jquery.min.js"></script>
    <script src="js/common.js"></script>
</head>

<body>
    <!-- 导航栏 -->
    <div class="nav">
        <img src="img/logo2.jpg" alt="">
        <span class="title">我的博客系统</span>
        <!-- 用来占据中间位置 -->
        <span class="spacer"></span>
        <a href="myblog_list.html">我的主页</a>
        <a href="blog_edit.html">写博客</a>
        <a href="javascript:logout()">注销</a>
        <!-- <a href="#">注销</a> -->
    </div>
    <!-- 版心 -->
    <div class="container">
        <!-- 右侧内容详情 -->
        <div class="container-right" style="width: 100%;">
            <!-- 每一篇博客包含标题, 摘要, 时间 -->
            <div id="artDiv">

            </div>

            <hr>
            <div class="blog-pagnation-wrapper">
                <button onclick="homePage()" class="blog-pagnation-item">首页</button>
                <button onclick="prevPage()" class="blog-pagnation-item">上一页</button>
                <button onclick="nextPage()" class="blog-pagnation-item">下一页</button>
                <button onclick="lastPage()" class="blog-pagnation-item">末页</button>
            </div>
        </div>
    </div>



</body>
</html>

编写 js 代码

创建三个全局变量,分别是 当前页数、每页显示最大文章数、文章总页数,通过这三个全局变量就可以实现4个按钮的分页功能。

  • 首页按钮:将当前页数设置为1,再请求服务器获取当前博客列表,请求中的参数为当前页数。
  • 上一页按钮:如果当前页数为1,则提示“当前已在首页”,若不在,则将请求服务器获取当前博客列表,请求中的参数为当前页数 + 1。
  • 下一页按钮:和上一页按钮同理
  • 末页按钮:将当前页设置为最后一页,并请求后端获取文章列表。
    <script>

        //当前页数
        var curpage = 1;
        //每页显示的最大文章数
        var psize = 3;
        //文章总页数       
        var pcount = 1;


        //获取博客列表
        function getArtList(pindex, psize) {
            jQuery.ajax({
                type: "GET",
                url: "/art/listbypage",
                data: {
                    "pindex": pindex,
                    "psize": psize
                },
                success: function (result) {
                    if (result != null && result.code == 200) {
                        //这里有两种情况,一种是有文章,一种是没有用户发表文章
                        if (result.data != null && result.data.list.length > 0) {
                            //有文章
                            var artListDiv = "";
                            for (var i = 0; i < result.data.list.length; i++) {
                                var artItem = result.data.list[i];
                                artListDiv += '<div class="blog">';
                                artListDiv += '<div class="title">' + artItem.title + '</div>';
                                artListDiv += '<div class="date">' + artItem.createtime + '</div>';
                                artListDiv += '<div class="desc">' + artItem.content + '</div>';
                                artListDiv += '<a href="blog_content.html?id=' + artItem.id + '" class="detail">查看全文 &gt;&gt;</a>'
                                artListDiv += '</div>';
                            }
                            //将 html 填充进去
                            jQuery("#artDiv").html(artListDiv);
                            //显示当前页数
                            jQuery("#page").text(" 第 " + curpage + " 页 ");
                            //总页数
                            pcount = result.data.pcount;
                        } else {
                            //无文章
                            jQuery("#artDiv").html("<h2>暂无文章</h2>");
                        }
                    } else {
                        alert("博客列表获取失败!");
                    }
                }
            });
        }
        //初始化文章列表首页
        getArtList(curpage, psize);

        //分页功能处理
        //首页按钮
        function homePage() {
            curpage = 1;
            getArtList(curpage, psize);
        }

        //上一页按钮
        function prevPage() {
            if (curpage == 1) {
                alert("当前已在首页!");
                return;
            } else {
                getArtList(--curpage, psize);
            }
        }

        //下一页按钮
        function nextPage() {
            if (curpage == pcount) {
                alert("当前已在最后一页!");
                return;
            } else {
                getArtList(++curpage, psize);
            }
        }

        //末页按钮
        function lastPage() {
            curpage = pcount;
            getArtList(curpage, psize);
        }

    </script>

服务器开发

服务器传入两个参数,反别是当前页码(pindex)、每页显示条数(psize),通过这两个参数如何实现分页查询呢(为什么要设计这两个参数)?

我们的服务器最后是要使用 MyBatis 对数据库进行修改的,那么分页的 sql 因该为 "select * from articleinfo limit #{psize} offset #{offset}",psize 我们以及约定好了(假设约定为3,也就是每页最多显示三条文章信息),那么 offset 的值怎么得来呢?

offset 是偏移量,也就是分页的起始下标(从0下标开始),就可以通过 当前页码(pindex)和 每页显示条数(psize) 计算得来!

这里需要一点数学推理,首先给出公式(方便后面理解): offset = (pindex - 1) * psize ,推理过程如下:

  • 假设当前页码(pindex)为 1,每页显示(psize) 3 条,那么 offset 就是 0,因为从第 0 条开始显示。
  • 假设当前页码(pindex)为 2,每页显示(psize) 3 条,那么 offset 就是 3,因为前三条数据(下标分别为:0、1、2)刚刚已经在第一页显示了,因此下一页就因该从下标为 3 的数据开始显示。
  • 往后以此类推...


    /**
     * 博客列表分页功能
     * @param pindex 当前页码
     * @param psize 每页显示条数(约定每页显示 3 条)
     * @return
     */
    //limit 3 offset 0
    //limit 3 offset 3
    //        offset 6
    //offset = (pindex - 1) * psize
    @RequestMapping("/listbypage")
    public AjaxResult getlistByPage(Integer pindex, Integer psize) {
        //非空校验
        if(pindex == null || pindex < 1) {
            pindex = 1;
        }
        if(psize == null || psize < 3) {
            psize = 3;
        }
        //公式计算得到 offset
        int offset = (pindex - 1) * psize;
        List<ArticleInfo> list = articleService.getListByPage(psize, offset);

        //当前列表有多少也
        //a.总共有多少条数据
        int totalCount = articleService.getArtAllCount();
        //b.总条数 / psize
        double pcountdb = totalCount / (psize * 1.0);
        //c.使用进1法得到总页数
        int pcount = (int) Math.ceil(pcountdb);
        HashMap<String, Object> result = new HashMap<>();
        result.put("list", list);
        result.put("pcount", pcount);
        return AjaxResult.success(result);
    }

前后端交互——两种前端得到 文章总页数 的方法,那种更合适?

两种方法分别如下

  1. 后端使用哈希表带上 文章总数和文章列表 传送给前端,前端进行一次 HTTP 请求完成分页功能的实现”。(刚刚所讲的分页功能就是这样实现的)
  2. 前后端使用两次  HTTP 请求完成分页功能,也就是前端发送两次 ajax 请求后端,一次是请求文章列表,一次是请求总页数。

第二种方法前端代码如下:

    <script>

        //当前页数
        var curpage = 1;
        //每页显示的最大文章数
        var psize = 3;
        //文章总页数       
        var pcount = 1;
        //文章总数
        var allCount = 1;
        function getAllCount() {
            jQuery.ajax({
                url: "/art/allcount" ,
                type: "GET",
                async: false, //async 是设置状态的(同步执行和异步执行),默认为 true 异步执行,有可能会出现"抢占执行"
                success: function (result) {
                    if (result != null && result.code == 200) {
                        allCount = result.data;
                    }
                }
            });
        }
         getAllCount();

        //获取博客列表
        function getArtList(pindex, psize) {
            jQuery.ajax({
                type: "GET",
                url: "/art/listbypage",
                data: {
                    "pindex": pindex,
                    "psize": psize
                },
                success: function (result) {
                    if (result != null && result.code == 200) {
                        //这里有两种情况,一种是有文章,一种是没有用户发表文章
                        if (result.data != null && result.data.length > 0) {
                            //有文章
                            var artListDiv = "";
                            for (var i = 0; i < result.data.length; i++) {
                                var artItem = result.data[i];
                                artListDiv += '<div class="blog">';
                                artListDiv += '<div class="title">' + artItem.title + '</div>';
                                artListDiv += '<div class="date">' + artItem.createtime + '</div>';
                                artListDiv += '<div class="desc">' + artItem.content + '</div>';
                                artListDiv += '<a href="blog_content.html?id=' + artItem.id + '" class="detail">查看全文 &gt;&gt;</a>'
                                artListDiv += '</div>';
                            }
                            //将 html 填充进去
                            jQuery("#artDiv").html(artListDiv);
                            //显示当前页数
                            jQuery("#page").text(" 第 " + curpage + " 页 ");
                        } else {
                            //无文章
                            jQuery("#artDiv").html("<h2>暂无文章</h2>");
                        }
                    } else {
                        alert("博客列表获取失败!");
                    }
                }
            });
        }
        //初始化首页
        getArtList(curpage, psize);

        //分页功能处理
        //首页按钮
        function homePage() {
            curpage = 1;
            getArtList(curpage, psize);
        }

        //上一页按钮
        function prevPage() {
            if (curpage == 1) {
                alert("当前已在首页!");
                return;
            } else {
                getArtList(--curpage, psize);
            }
        }

        //下一页按钮
        function nextPage() {
            if (curpage == pcount) {
                alert("当前已在最后一页!");
                return;
            } else {
                getArtList(++curpage, psize);
            }
        }

        //末页按钮
        function lastPage() {
            curpage = pcount;
            getArtList(curpage, psize);
        }

    </script>

那种方法更好呢?

两种方法都可以,没有明确的好坏之分,要说有的话,方法二需要进行两次 HTTP 请求,而 HTTP 建立连接时需要时间的,因此在效率上,第一种方式更优,并且企业中最常用的也是第一种方式。

小结

如果想了解项目的样式以及源码,请私信~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

补充C语言

1.关键字 前言: C90一共有32个关键字,C99比C90多了5个关键字,但主流的编译器对C99关键字支持的不是特别好, 所以后面主要以C90的32个关键字为标准1.1认识auto关键字 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() {int i 0;auto int j 0;retur…

为什么要参加软考?软考如何备考?

软考是指软件工程师职业资格考试&#xff0c;它是由国家人力资源和社会保障部颁发的国家级职业资格证书&#xff0c;是目前我国最具权威性的计算机职业资格证书之一。软考考试内容丰富&#xff0c;包括软件工程、软件测试、软件项目管理、数据库等多个方面&#xff0c;考试难度…

Java分布式事务(七)

文章目录 🔥Seata提供XA模式实现分布式事务_业务说明🔥Seata提供XA模式实现分布式事务_下载启动Seata服务🔥Seata提供XA模式实现分布式事务_搭建聚合父工程构建🔥Seata提供XA模式实现分布式事务_转账功能实现上🔥Seata提供XA模式实现分布式事务_转账功能实现下🔥Se…

Qt-Web混合开发-CEF加载网页简单示例(12)

Qt-Web混合开发-CEF加载网页简单示例&#x1f499;&#x1f353; 文章目录Qt-Web混合开发-CEF加载网页简单示例&#x1f499;&#x1f353;1、概述&#x1f41b;&#x1f986;2、实现效果&#x1f605;&#x1f64f;3、实现功能&#x1f42e;&#x1f434;4、Qt部分关键代码&am…

2023/4/2总结

题解 线段树OR树状数组 - Virtual Judge (vjudge.net) 正如这道题目一样&#xff0c;我的心情也如此。 1.这道题是线段树问题&#xff0c;更改学生值即可&#xff0c;不需要用到懒惰标记。 2.再去按照区间查找即可。&#xff08;多组输入&#xff0c;拿20多次提交换来的&am…

2023大数据开发就业前景怎么样?

大数据开发就业班正在火热招生中。 大数据开发做什么&#xff1f; 大数据开发分两类&#xff0c;编写Hadoop、Spark的应用程序和对大数据处理系统本身进行开发。大数据开发工程师主要负责公司大数据平台的开发和维护、相关工具平台的架构设计与产品开发、网络日志大…

【算法基础】(一)基础算法 --- 位运算

✨个人主页&#xff1a;bit me ✨当前专栏&#xff1a;算法基础 &#x1f525;专栏简介&#xff1a;该专栏主要更新一些基础算法题&#xff0c;有参加蓝桥杯等算法题竞赛或者正在刷题的铁汁们可以关注一下&#xff0c;互相监督打卡学习 &#x1f339; &#x1f339; &#x1f3…

C语言函数大全--d开头的函数

C语言函数大全 本篇介绍C语言函数大全–d开头的函数 1. detectgraph 1.1 函数说明 函数声明函数功能void detectgraph(int *graphdriver, int *graphmode);通过检测硬件确定图形驱动程序和模式 1.2 演示示例 #include <graphics.h> #include <stdlib.h> #incl…

【Java 并发编程】一文读懂线程、协程、守护线程

一文读懂线程、协程、守护线程1. 线程的调度1.1 协同式线程调度1.2 抢占式线程调度1.3 设置线程的优先级2. 线程的实现模型和协程2.1 内核线程实现2.2 用户线程实现2.3 混合实现2.4 Java 线程的实现2.5 协程2.5.1 出现的原因2.5.2 什么是协程2.5.3 Java19 虚拟线程 - 协程的复苏…

Activiti7与Spring、Spring Boot整合开发

Activiti整合Spring 一、Activiti与Spring整合开发 1.1 Activiti与Spring整合的配置 1)、在pom.xml文件引入坐标 如下 <properties><slf4j.version>1.6.6</slf4j.version><log4j.version>1.2.12</log4j.version> </properties> <d…

【源码教程案例】AI绘画与安全在未来主要方向有哪些?

AI绘画在未来有许多潜在的发展方向,以下是一些可能的重点领域 高质量图像生成:随着生成模型的不断改进,未来的AI绘画可能会产生更高质量、更真实的图像,以满足各种应用场景的需求。 个性化创作:AI绘画可以通过用户的个性化偏好和需求来定制艺术作品。这种定制可能包括颜…

【gitlab部署】centos8安装gitlab(搭建属于自己的代码服务器)

这里写目录标题部署篇序言要求检查系统是否安装OpenSSH防火墙问题准备gitlab.rb 配置坑点一忘记root密码重置使用篇gitlab转换成中文git关闭注册入口创建用户部署篇 序言 在团队开发过程中&#xff0c;想要拥有高效的开发效率&#xff0c;选择一个好的代码开发工具是必不可少的…

JUnit5用户手册~并行执行

两种运行模式 SAME_THREAD&#xff1a;默认的&#xff0c;测试方法在同一个线程CONCURRENT&#xff1a;并行执行&#xff0c;除非有资源锁junit-platform.properties配置参数配置所有测试方法都并行 junit.jupiter.execution.parallel.enabled true junit.jupiter.execution.…

Kafka3.0.0版本——生产者分区及分区策略

目录一、生产者分区优点二、生产者发送消息的分区策略2.1、默认的分区器2.2、指定分区(partition)值2.3、没有指明分区(partition)值&#xff0c;但有key 的情况2.4、 既没有分区(partition)值&#xff0c;又没有key 值的情况三、指定分区(partition)值的代码示例四、没有指明分…

vscode折叠展开快捷键

1.折叠所有代码 (按住ctrl 分别点击k和0) ctrlk,ctrl0 2.展开所有代码 (按住ctrl 分别点击k和j) ctrlk,ctrlj 3. 折叠鼠标竖线所在位置的节点以及当前节点下的子节点&#xff08;递归折叠&#xff09; ctrlk,ctrl[ 4. 展开鼠标竖线所在位置的节点以及当前节点下的子节点&#…

OpenFeign 源码解读:动态代理+负载均衡实现

OpenFeign使用EnableFeignClients开启服务&#xff0c;该注解标有Import(FeignClientsRegistrar.class)&#xff0c;该ImportBeanDefinitionRegistrar会利用扫描路径的方式扫描java文件中带有的FeignClient(...)的接口&#xff0c;关于这种扫描注解的方式&#xff0c;我仿照写了…

软件测试 - 测试用例常见面试题

1.测试用例的要素测试用例是为了实施测试而向被测试的系统提供的一组集合, 这组集合包含 : 测试环境, 操作步骤, 测试数据, 预期结果等要素.例如 : 在 B 站输入框输入一个空格, 检查结果测试用例标题 : 输入框输入空格测试环境 : Windows 系统, 谷歌浏览器-版本 111.0.5563.65&…

固态硬盘需要分区吗 固态硬盘怎么分区

磁盘分区是在磁盘中划分几个逻辑部分&#xff0c;来更充分的利用磁盘空间&#xff0c;对保存的数据进行分类储存&#xff0c;方便使用。今天小编给大家介绍一下&#xff0c;固态硬盘需要分区吗&#xff0c;固态硬盘怎么分区。 一、固态硬盘需要分区吗 固态硬盘是需要分区的&a…

Redis:redis通用命令;redis常见数据结构;redis客户端;redis的序列化

一、redis命令 1.redis通用命令 Redis 通用命令是一些 Redis 下可以作用在常用数据结构上的常用命令和一些基础的命令 常见的命令有&#xff1a; keys 查看符合模板的所有key&#xff0c;不建议在生产环境设备上使用&#xff0c;因为keys会模式匹配所有符合条件的key&#…

js常见的9种报错记录一下

js常见报错语法错误(SyntaxError)类型错误(TypeError)引用错误(ReferenceError)范围错误(RangeError)运行时错误(RuntimeError)网络错误&#xff08;NetworkError&#xff09;内部错误&#xff08;InternalError&#xff09;URI错误&#xff08;URIError&#xff09;eval错误&a…