目录
项目背景
核心技术
项目页面设计
注册页面
登录页面
博客列表页
博客详情页
个人博客列表页
个人博客发布页
个人博客修改页
项目模块与需求分析
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);
}
}
加盐加密、解密模块
加密
加密思路
加密的主要有以下几步:
- 产生随机盐值(32位)。解释:这里可以使用 UUID 中的 randomUUID 方法生成一个长度为 36 位的随机盐值(格式为xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)),而我们约定的格式中(完全由自己定义)不需要"-",因此将产生的随机盐值先使用 toString 方法转化成字符串,最后用 replaceAll 方法将字符串中的 "-" 用空字符串代替即可。
- 将盐值和明文拼接起来,然后整体使用 md5 加密,得到密文(32位)。解释:这里就是对明文进行加密的过程。
- 根据自己约定的格式,使用 “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的重载)
* 此方法在验证密码的适合需要(将用户输入的密码使用同样的盐值加密后对比)
* @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">查看全文 >></a>';
artListDiv += '<a href="blog_edit.html?id=' + artItem.id + '" class="detail">修改文章 >></a>';
artListDiv += '<a href="javascript:delArt(' + artItem.id + ')" class="detail">删除文章 >></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> 阅读量:<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">查看全文 >></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);
}
前后端交互——两种前端得到 文章总页数 的方法,那种更合适?
两种方法分别如下
- 后端使用哈希表带上 文章总数和文章列表 传送给前端,前端进行一次 HTTP 请求完成分页功能的实现”。(刚刚所讲的分页功能就是这样实现的)
- 前后端使用两次 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">查看全文 >></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 建立连接时需要时间的,因此在效率上,第一种方式更优,并且企业中最常用的也是第一种方式。
小结
如果想了解项目的样式以及源码,请私信~